colloq 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- colloq/__init__.py +12 -0
- colloq/__main__.py +315 -0
- colloq/_app/.colloq-dist.json +66 -0
- colloq/_app/LICENSE +21 -0
- colloq/_app/THIRD_PARTY_NOTICES.md +106 -0
- colloq/_app/cli/colloq.mjs +2517 -0
- colloq/_app/cli/launch.mjs +1572 -0
- colloq/_app/cli/package.json +5 -0
- colloq/_app/cli/public-url-lease.mjs +198 -0
- colloq/_app/kernel/Dockerfile +93 -0
- colloq/_app/kernel/environments/base-gpu.txt +21 -0
- colloq/_app/kernel/environments/base.txt +12 -0
- colloq/_app/kernel/environments/cv.txt +15 -0
- colloq/_app/kernel/environments/gpu.txt +17 -0
- colloq/_app/kernel/environments/kaggle-base.txt +20 -0
- colloq/_app/kernel/environments/pvz-geo.txt +6 -0
- colloq/_app/kernel/requirements.txt +18 -0
- colloq/_app/package.json +20 -0
- colloq/_app/scripts/backup-local.sh +165 -0
- colloq/_app/scripts/backup.sh +34 -0
- colloq/_app/scripts/cluster.sh +492 -0
- colloq/_app/scripts/dns.sh +251 -0
- colloq/_app/scripts/host.sh +1514 -0
- colloq/_app/scripts/lib.sh +62 -0
- colloq/_app/scripts/restore.sh +338 -0
- colloq/_app/scripts/runtime-backup.py +373 -0
- colloq/_app/scripts/state-lock.py +85 -0
- colloq/_app/server/assets/fonts/HSESans-Black.otf +0 -0
- colloq/_app/server/assets/fonts/HSESans-Bold.otf +0 -0
- colloq/_app/server/assets/fonts/HSESans-Regular.otf +0 -0
- colloq/_app/server/assets/fonts/JetBrainsMono-Medium.ttf +0 -0
- colloq/_app/server/assets/fonts/JetBrainsMono-OFL.txt +93 -0
- colloq/_app/server/dist/local-cleanup.js +15987 -0
- colloq/_app/server/dist/runtime-smoke.js +17026 -0
- colloq/_app/server/dist/server.js +54333 -0
- colloq/_app/server/package.json +6 -0
- colloq/_app/web/dist/assets/AdminScreen-C-cg0GlJ.css +1 -0
- colloq/_app/web/dist/assets/AdminScreen-C-cg0GlJ.css.br +0 -0
- colloq/_app/web/dist/assets/AdminScreen-C-cg0GlJ.css.gz +0 -0
- colloq/_app/web/dist/assets/AdminScreen-fu4cB6Ra.js +105 -0
- colloq/_app/web/dist/assets/AdminScreen-fu4cB6Ra.js.br +0 -0
- colloq/_app/web/dist/assets/AdminScreen-fu4cB6Ra.js.gz +0 -0
- colloq/_app/web/dist/assets/App-CMXNqOjp.css +1 -0
- colloq/_app/web/dist/assets/App-CMXNqOjp.css.br +0 -0
- colloq/_app/web/dist/assets/App-CMXNqOjp.css.gz +0 -0
- colloq/_app/web/dist/assets/App-DvCPzIRO.js +26 -0
- colloq/_app/web/dist/assets/App-DvCPzIRO.js.br +0 -0
- colloq/_app/web/dist/assets/App-DvCPzIRO.js.gz +0 -0
- colloq/_app/web/dist/assets/CellOutputs-Dl-AZL45.js +42 -0
- colloq/_app/web/dist/assets/CellOutputs-Dl-AZL45.js.br +0 -0
- colloq/_app/web/dist/assets/CellOutputs-Dl-AZL45.js.gz +0 -0
- colloq/_app/web/dist/assets/CommandPalette-DhBGho7_.js +3 -0
- colloq/_app/web/dist/assets/CommandPalette-DhBGho7_.js.br +0 -0
- colloq/_app/web/dist/assets/CommandPalette-DhBGho7_.js.gz +0 -0
- colloq/_app/web/dist/assets/CompetitionsScreen-Vu3RpHmf.js +3 -0
- colloq/_app/web/dist/assets/CompetitionsScreen-Vu3RpHmf.js.br +0 -0
- colloq/_app/web/dist/assets/CompetitionsScreen-Vu3RpHmf.js.gz +0 -0
- colloq/_app/web/dist/assets/ConsoleView-B2uyYvFh.js +1 -0
- colloq/_app/web/dist/assets/ConsoleView-B2uyYvFh.js.br +0 -0
- colloq/_app/web/dist/assets/ConsoleView-B2uyYvFh.js.gz +0 -0
- colloq/_app/web/dist/assets/ConsoleView-DLbp0Jul.css +1 -0
- colloq/_app/web/dist/assets/DependencyBundleCard-B392XMVZ.js +4 -0
- colloq/_app/web/dist/assets/DependencyBundleCard-B392XMVZ.js.br +0 -0
- colloq/_app/web/dist/assets/DependencyBundleCard-B392XMVZ.js.gz +0 -0
- colloq/_app/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- colloq/_app/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- colloq/_app/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- colloq/_app/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- colloq/_app/web/dist/assets/PultWindow-B3cc8537.css +1 -0
- colloq/_app/web/dist/assets/PultWindow-B3cc8537.css.br +0 -0
- colloq/_app/web/dist/assets/PultWindow-B3cc8537.css.gz +0 -0
- colloq/_app/web/dist/assets/PultWindow-DPOUhSqt.js +8 -0
- colloq/_app/web/dist/assets/PultWindow-DPOUhSqt.js.br +0 -0
- colloq/_app/web/dist/assets/PultWindow-DPOUhSqt.js.gz +0 -0
- colloq/_app/web/dist/assets/ReaderScreen-CV_0VAvW.js +9 -0
- colloq/_app/web/dist/assets/ReaderScreen-CV_0VAvW.js.br +0 -0
- colloq/_app/web/dist/assets/ReaderScreen-CV_0VAvW.js.gz +0 -0
- colloq/_app/web/dist/assets/SessionScreen-BrcB6CLH.js +132 -0
- colloq/_app/web/dist/assets/SessionScreen-BrcB6CLH.js.br +0 -0
- colloq/_app/web/dist/assets/SessionScreen-BrcB6CLH.js.gz +0 -0
- colloq/_app/web/dist/assets/SessionScreen-Dek9dK3C.css +1 -0
- colloq/_app/web/dist/assets/SessionScreen-Dek9dK3C.css.br +0 -0
- colloq/_app/web/dist/assets/SessionScreen-Dek9dK3C.css.gz +0 -0
- colloq/_app/web/dist/assets/actions-DLPyolgo.js +1 -0
- colloq/_app/web/dist/assets/admin-en-BHmR7Co0.js +9 -0
- colloq/_app/web/dist/assets/admin-en-BHmR7Co0.js.br +0 -0
- colloq/_app/web/dist/assets/admin-en-BHmR7Co0.js.gz +0 -0
- colloq/_app/web/dist/assets/admin-ru-Dr6Z36Zi.js +11 -0
- colloq/_app/web/dist/assets/admin-ru-Dr6Z36Zi.js.br +0 -0
- colloq/_app/web/dist/assets/admin-ru-Dr6Z36Zi.js.gz +0 -0
- colloq/_app/web/dist/assets/codemirror-6LUh0q1l.js +32 -0
- colloq/_app/web/dist/assets/codemirror-6LUh0q1l.js.br +0 -0
- colloq/_app/web/dist/assets/codemirror-6LUh0q1l.js.gz +0 -0
- colloq/_app/web/dist/assets/competitions-en-zw57uAGL.js +5 -0
- colloq/_app/web/dist/assets/competitions-en-zw57uAGL.js.br +0 -0
- colloq/_app/web/dist/assets/competitions-en-zw57uAGL.js.gz +0 -0
- colloq/_app/web/dist/assets/competitions-ru-BBQUjAx0.js +5 -0
- colloq/_app/web/dist/assets/competitions-ru-BBQUjAx0.js.br +0 -0
- colloq/_app/web/dist/assets/competitions-ru-BBQUjAx0.js.gz +0 -0
- colloq/_app/web/dist/assets/editor-theme-CVYtpZDy.js +1 -0
- colloq/_app/web/dist/assets/editor-theme-CVYtpZDy.js.br +0 -0
- colloq/_app/web/dist/assets/editor-theme-CVYtpZDy.js.gz +0 -0
- colloq/_app/web/dist/assets/index-CwbYK31L.css +1 -0
- colloq/_app/web/dist/assets/index-CwbYK31L.css.br +0 -0
- colloq/_app/web/dist/assets/index-CwbYK31L.css.gz +0 -0
- colloq/_app/web/dist/assets/index-Di16xdmz.js +2 -0
- colloq/_app/web/dist/assets/index-Di16xdmz.js.br +0 -0
- colloq/_app/web/dist/assets/index-Di16xdmz.js.gz +0 -0
- colloq/_app/web/dist/assets/katex-CS_ezkZp.css +1 -0
- colloq/_app/web/dist/assets/katex-CS_ezkZp.css.br +0 -0
- colloq/_app/web/dist/assets/katex-CS_ezkZp.css.gz +0 -0
- colloq/_app/web/dist/assets/katex-DLorvT8M.js +257 -0
- colloq/_app/web/dist/assets/katex-DLorvT8M.js.br +0 -0
- colloq/_app/web/dist/assets/katex-DLorvT8M.js.gz +0 -0
- colloq/_app/web/dist/assets/pdf-DEp6sk-T.js +55 -0
- colloq/_app/web/dist/assets/pdf-DEp6sk-T.js.br +0 -0
- colloq/_app/web/dist/assets/pdf-DEp6sk-T.js.gz +0 -0
- colloq/_app/web/dist/assets/persistence.svelte-vcuCBcT5.js +1 -0
- colloq/_app/web/dist/assets/persistence.svelte-vcuCBcT5.js.br +0 -0
- colloq/_app/web/dist/assets/persistence.svelte-vcuCBcT5.js.gz +0 -0
- colloq/_app/web/dist/assets/reader-en-UCGE3U6V.js +3 -0
- colloq/_app/web/dist/assets/reader-en-UCGE3U6V.js.br +0 -0
- colloq/_app/web/dist/assets/reader-en-UCGE3U6V.js.gz +0 -0
- colloq/_app/web/dist/assets/reader-ru-CaiNj0K8.js +3 -0
- colloq/_app/web/dist/assets/reader-ru-CaiNj0K8.js.br +0 -0
- colloq/_app/web/dist/assets/reader-ru-CaiNj0K8.js.gz +0 -0
- colloq/_app/web/dist/assets/render-CjP3RuRT.js +155 -0
- colloq/_app/web/dist/assets/render-CjP3RuRT.js.br +0 -0
- colloq/_app/web/dist/assets/render-CjP3RuRT.js.gz +0 -0
- colloq/_app/web/dist/assets/render.svelte-B4_hAJmX.js +0 -0
- colloq/_app/web/dist/assets/render.svelte-B4_hAJmX.js.br +0 -0
- colloq/_app/web/dist/assets/render.svelte-B4_hAJmX.js.gz +0 -0
- colloq/_app/web/dist/assets/room-en-Bh8L6NcO.js +3 -0
- colloq/_app/web/dist/assets/room-en-Bh8L6NcO.js.br +0 -0
- colloq/_app/web/dist/assets/room-en-Bh8L6NcO.js.gz +0 -0
- colloq/_app/web/dist/assets/room-ru-Bar2GgS0.js +3 -0
- colloq/_app/web/dist/assets/room-ru-Bar2GgS0.js.br +0 -0
- colloq/_app/web/dist/assets/room-ru-Bar2GgS0.js.gz +0 -0
- colloq/_app/web/dist/assets/seminar-link-CRaZl9Ku.js +1 -0
- colloq/_app/web/dist/assets/seminar-link-CRaZl9Ku.js.br +0 -0
- colloq/_app/web/dist/assets/seminar-link-CRaZl9Ku.js.gz +0 -0
- colloq/_app/web/dist/assets/seminar-link-DoTugxxA.css +1 -0
- colloq/_app/web/dist/assets/seminar-link-DoTugxxA.css.br +0 -0
- colloq/_app/web/dist/assets/seminar-link-DoTugxxA.css.gz +0 -0
- colloq/_app/web/dist/assets/yjs-rjB3hdnL.js +6 -0
- colloq/_app/web/dist/assets/yjs-rjB3hdnL.js.br +0 -0
- colloq/_app/web/dist/assets/yjs-rjB3hdnL.js.gz +0 -0
- colloq/_app/web/dist/fonts/JetBrainsMono-OFL.txt +93 -0
- colloq/_app/web/dist/fonts/hse-sans-400.woff2 +0 -0
- colloq/_app/web/dist/fonts/hse-sans-600.woff2 +0 -0
- colloq/_app/web/dist/fonts/hse-sans-700.woff2 +0 -0
- colloq/_app/web/dist/fonts/hse-sans-900.woff2 +0 -0
- colloq/_app/web/dist/fonts/jetbrains-mono-cyrillic-ext.woff2 +0 -0
- colloq/_app/web/dist/fonts/jetbrains-mono-cyrillic.woff2 +0 -0
- colloq/_app/web/dist/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- colloq/_app/web/dist/fonts/jetbrains-mono-latin.woff2 +0 -0
- colloq/_app/web/dist/index.html +268 -0
- colloq/_app/web/dist/og/colloq.png +0 -0
- colloq/_app/web/dist/pdf/pdf.worker.min.mjs +29 -0
- colloq/_app/web/dist/pdf/pdf.worker.min.mjs.br +0 -0
- colloq/_app/web/dist/pdf/pdf.worker.min.mjs.gz +0 -0
- colloq/_app/web/dist/plotly/plotly.min.js +3620 -0
- colloq/_app/web/dist/plotly/plotly.min.js.br +0 -0
- colloq/_app/web/dist/plotly/plotly.min.js.gz +0 -0
- colloq/_version.py +5 -0
- colloq-0.2.0.dist-info/METADATA +124 -0
- colloq-0.2.0.dist-info/RECORD +224 -0
- colloq-0.2.0.dist-info/WHEEL +4 -0
- colloq-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// scripts/public-url-lease.mts
|
|
2
|
+
import fs2 from "node:fs";
|
|
3
|
+
import path2 from "node:path";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
+
|
|
6
|
+
// server/src/local/public-url-lease.ts
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
|
|
11
|
+
// shared/local-public-url-lease.ts
|
|
12
|
+
function publicLeaseAddress(value) {
|
|
13
|
+
if (typeof value !== "string") return null;
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(value);
|
|
16
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) return null;
|
|
17
|
+
return url.href.replace(/\/+$/, "");
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function parsePublicUrlLease(raw) {
|
|
23
|
+
try {
|
|
24
|
+
const lease = JSON.parse(raw ?? "");
|
|
25
|
+
if (!lease || typeof lease.runId !== "string" || !lease.runId || typeof lease.owner !== "string" || !lease.owner || !Number.isFinite(lease.expiresAt)) return null;
|
|
26
|
+
const url = publicLeaseAddress(lease.url);
|
|
27
|
+
return url ? { runId: lease.runId, owner: lease.owner, expiresAt: lease.expiresAt, url } : null;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// server/src/local/public-url-lease.ts
|
|
34
|
+
var LEASE_TTL_MS = 15e3;
|
|
35
|
+
function read(file) {
|
|
36
|
+
try {
|
|
37
|
+
return parsePublicUrlLease(fs.readFileSync(file, "utf8"));
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function locked(file, operation) {
|
|
43
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 448 });
|
|
44
|
+
const lock = `${file}.lock`;
|
|
45
|
+
try {
|
|
46
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > 6e4) fs.rmdirSync(lock);
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
fs.mkdirSync(lock, { mode: 448 });
|
|
50
|
+
try {
|
|
51
|
+
return operation();
|
|
52
|
+
} finally {
|
|
53
|
+
fs.rmdirSync(lock);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function write(file, lease) {
|
|
57
|
+
const temp = `${file}.${randomUUID()}.tmp`;
|
|
58
|
+
try {
|
|
59
|
+
fs.writeFileSync(temp, JSON.stringify(lease) + "\n", { mode: 384, flag: "wx" });
|
|
60
|
+
fs.renameSync(temp, file);
|
|
61
|
+
} finally {
|
|
62
|
+
try {
|
|
63
|
+
fs.unlinkSync(temp);
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function acquireLease(file, value, now = Date.now(), ttl = LEASE_TTL_MS) {
|
|
69
|
+
const url = publicLeaseAddress(value.url);
|
|
70
|
+
if (!url || !value.owner || !value.runId) throw new Error("Invalid public URL lease");
|
|
71
|
+
locked(file, () => {
|
|
72
|
+
const previous = read(file);
|
|
73
|
+
if (previous && previous.expiresAt > now) throw new Error("A tunnel already owns the public URL");
|
|
74
|
+
write(file, { ...value, url, expiresAt: now + ttl });
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function renewLease(file, runId, owner, now = Date.now(), ttl = LEASE_TTL_MS) {
|
|
78
|
+
return locked(file, () => {
|
|
79
|
+
const lease = read(file);
|
|
80
|
+
if (!lease || lease.runId !== runId || lease.owner !== owner || lease.expiresAt <= now) return false;
|
|
81
|
+
write(file, { ...lease, expiresAt: now + ttl });
|
|
82
|
+
return true;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function releaseLease(file, runId, owner) {
|
|
86
|
+
return locked(file, () => {
|
|
87
|
+
const lease = read(file);
|
|
88
|
+
if (!lease || lease.runId !== runId || lease.owner !== owner) return false;
|
|
89
|
+
fs.unlinkSync(file);
|
|
90
|
+
return true;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// scripts/public-url-lease.mts
|
|
95
|
+
function alive(pid) {
|
|
96
|
+
if (!Number.isSafeInteger(pid) || pid <= 1) return false;
|
|
97
|
+
try {
|
|
98
|
+
process.kill(pid, 0);
|
|
99
|
+
return true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function verify(url, runId) {
|
|
105
|
+
const response = await fetch(`${url.replace(/\/+$/, "")}/api/health`, { signal: AbortSignal.timeout(3e3) });
|
|
106
|
+
const health = await response.json();
|
|
107
|
+
if (!runId || health.localRunId !== runId) throw new Error("Local session identity does not match the running server");
|
|
108
|
+
}
|
|
109
|
+
async function servedByAnother(port, runId) {
|
|
110
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return false;
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/health`, { signal: AbortSignal.timeout(3e3) });
|
|
113
|
+
const health = await response.json();
|
|
114
|
+
return typeof health.version === "string" && (health.localRunId ?? null) !== runId;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function main() {
|
|
120
|
+
const [command, ...args] = process.argv.slice(2);
|
|
121
|
+
if (command === "discover") {
|
|
122
|
+
let receipt;
|
|
123
|
+
if (process.env.COLLOQ_LOCAL_SESSION === "1") {
|
|
124
|
+
receipt = { runId: process.env.COLLOQ_LOCAL_RUN_ID, leaseFile: process.env.COLLOQ_PUBLIC_URL_LEASE_FILE, url: process.env.COLLOQ_LOCAL_URL, port: Number(process.env.PORT || 3e3), dataDir: process.env.DATA_DIR || path2.resolve("data") };
|
|
125
|
+
} else {
|
|
126
|
+
if (!fs2.existsSync(args[0])) {
|
|
127
|
+
process.exitCode = 2;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
receipt = JSON.parse(fs2.readFileSync(args[0], "utf8"));
|
|
131
|
+
if (!alive(Number(receipt.pid))) {
|
|
132
|
+
if (await servedByAnother(Number(receipt.port), receipt.runId)) {
|
|
133
|
+
process.exitCode = 2;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
throw new Error("Local session is no longer running; start colloq first");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!Number.isInteger(receipt.port) || receipt.port < 1 || receipt.port > 65535) throw new Error("Invalid local session port");
|
|
140
|
+
const values = [receipt.runId, receipt.leaseFile, receipt.url, String(receipt.port), receipt.dataDir];
|
|
141
|
+
if (values.some((v) => typeof v !== "string" || !v || /[\t\r\n]/.test(v))) throw new Error("Invalid local session receipt");
|
|
142
|
+
const local = new URL(receipt.url);
|
|
143
|
+
if (local.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(local.hostname)) throw new Error("Invalid local session URL");
|
|
144
|
+
await verify(`http://127.0.0.1:${receipt.port}`, receipt.runId);
|
|
145
|
+
process.stdout.write([...values, local.port || "80"].join(" ") + "\n");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const [file, runId, owner] = args;
|
|
149
|
+
if (!file || !runId || !owner) throw new Error("Expected lease file, run ID and owner");
|
|
150
|
+
if (command === "acquire") {
|
|
151
|
+
await verify(args[4], runId);
|
|
152
|
+
acquireLease(file, { runId, owner, url: args[3] });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (command === "release") {
|
|
156
|
+
releaseLease(file, runId, owner);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (command !== "watch") throw new Error("Unknown lease command");
|
|
160
|
+
const tunnelPid = Number(args[3]), hostPid = Number(args[4]), healthUrl = args[5];
|
|
161
|
+
let stopping = false;
|
|
162
|
+
const finish = () => {
|
|
163
|
+
if (stopping) return;
|
|
164
|
+
stopping = true;
|
|
165
|
+
try {
|
|
166
|
+
releaseLease(file, runId, owner);
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
if (alive(tunnelPid)) process.kill(tunnelPid, "SIGTERM");
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
process.once("SIGINT", finish);
|
|
175
|
+
process.once("SIGTERM", finish);
|
|
176
|
+
let lastHealthy = Date.now();
|
|
177
|
+
try {
|
|
178
|
+
while (!stopping) {
|
|
179
|
+
if (!alive(tunnelPid) || !alive(hostPid)) break;
|
|
180
|
+
try {
|
|
181
|
+
await verify(healthUrl, runId);
|
|
182
|
+
lastHealthy = Date.now();
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof Error && /identity does not match/.test(error.message) || Date.now() - lastHealthy >= LEASE_TTL_MS) throw error;
|
|
185
|
+
await delay(1e3);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (stopping || !renewLease(file, runId, owner)) break;
|
|
189
|
+
await delay(3e3);
|
|
190
|
+
}
|
|
191
|
+
} finally {
|
|
192
|
+
finish();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
main().catch((error) => {
|
|
196
|
+
console.error(error instanceof Error ? error.message : error);
|
|
197
|
+
process.exitCode = 1;
|
|
198
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Поверх чего собираемся — параметр, и это весь механизм наследования.
|
|
2
|
+
#
|
|
3
|
+
# Родитель по умолчанию — python-slim, то есть ровно прежняя сборка. Но им
|
|
4
|
+
# может быть и готовый colloq-образ (`--build-arg PARENT=colloq-kernel:base-gpu`),
|
|
5
|
+
# и тогда тот же самый файл делает не базу, а тонкий слой поверх неё: torch с
|
|
6
|
+
# CUDA остаётся в родителе и не ставится заново.
|
|
7
|
+
ARG PARENT=python:3.11-slim-bookworm
|
|
8
|
+
FROM ${PARENT}
|
|
9
|
+
|
|
10
|
+
# Родитель может быть нашим собственным образом, а в нём последним стоит
|
|
11
|
+
# `USER runner` — от него ни apt, ни pip не пройдут. На python-slim эта строка
|
|
12
|
+
# не меняет ничего: там и так root.
|
|
13
|
+
USER root
|
|
14
|
+
|
|
15
|
+
# MPLBACKEND здесь НЕТ, и это правка по живому семинару.
|
|
16
|
+
#
|
|
17
|
+
# Стояло `MPLBACKEND=Agg` — переменная действует на весь контейнер, в том числе
|
|
18
|
+
# на ядро, а Agg рисует в файл и не отдаёт ничего наружу. Значит `plt.show()` в
|
|
19
|
+
# ячейке не давал ни одного сообщения display_data: график не появлялся, ошибки
|
|
20
|
+
# не было, ячейка честно отчитывалась «выполнена». Проверено на живой машине:
|
|
21
|
+
# с Agg ядро отвечает одним `stream`, без переменной — `display_data` с
|
|
22
|
+
# image/png.
|
|
23
|
+
#
|
|
24
|
+
# Не задано — и правильно: внутри ядра matplotlib сам берёт inline-бэкенд
|
|
25
|
+
# (matplotlib_inline приезжает с ipykernel), а в обычном `python script.py`,
|
|
26
|
+
# где экрана нет, — тот же Agg. Обе стороны получают то, что им нужно, без
|
|
27
|
+
# переменной; задать её глобально значит выбрать одно из двух за оба случая.
|
|
28
|
+
# А PLOTLY_RENDERER — есть, и это правка ровно по тому же месту, что абзац выше.
|
|
29
|
+
#
|
|
30
|
+
# Без неё plotly решает сам, чем отдавать фигуру, и решает по-разному в разных
|
|
31
|
+
# версиях: 7.x в ipykernel отдаёт только `application/vnd.plotly.v1+json` — то,
|
|
32
|
+
# из чего тетрадь рисует интерактивный график, — а 5.x добавляет к нему
|
|
33
|
+
# `text/html` со скриптом и первым кадром высылает ВЕСЬ бандл plotly.js, пять
|
|
34
|
+
# мегабайт. Скрипты из вывода Colloq не исполняет никогда (SECURITY.md), так
|
|
35
|
+
# что эти пять мегабайт — чистый вес в общем документе, который едет каждому в
|
|
36
|
+
# комнате.
|
|
37
|
+
#
|
|
38
|
+
# `plotly_mimetype` — не запрет, а умолчание: студент перебивает его строкой
|
|
39
|
+
# `pio.renderers.default = "png"`, и это продолжает работать. Ту же переменную
|
|
40
|
+
# ставит сервер при подъёме контейнера комнаты (kernel/pool.ts · runArgs) —
|
|
41
|
+
# ради чужих окружений, собранных без этого файла.
|
|
42
|
+
ENV PYTHONUNBUFFERED=1 \
|
|
43
|
+
PIP_NO_CACHE_DIR=1 \
|
|
44
|
+
PLOTLY_RENDERER=plotly_mimetype
|
|
45
|
+
|
|
46
|
+
# Оба шага ниже повторяются на каждом слое цепочки, и это дёшево: на готовом
|
|
47
|
+
# родителе curl уже стоит (apt-get тратит секунды на индексы и отвечает
|
|
48
|
+
# «already the newest version»), а pip на каждую строку базы отвечает
|
|
49
|
+
# «Requirement already satisfied» и ничего не качает. Минуты стоит только то,
|
|
50
|
+
# чего в родителе нет, — ради этого наследование и заведено.
|
|
51
|
+
RUN apt-get update \
|
|
52
|
+
&& apt-get install -y --no-install-recommends curl libgomp1 \
|
|
53
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
54
|
+
|
|
55
|
+
COPY requirements.txt /tmp/requirements.txt
|
|
56
|
+
RUN pip install --no-cache-dir -r /tmp/requirements.txt
|
|
57
|
+
|
|
58
|
+
# Окружение семинара — слой поверх базы, а не вместо неё.
|
|
59
|
+
#
|
|
60
|
+
# Два слоя, а не один, ради кеша: база одинакова у всех окружений, поэтому
|
|
61
|
+
# docker переиспользует её, и «завести окружение» стоит только тех пакетов,
|
|
62
|
+
# которых в базе нет. Один общий слой пересобирался бы целиком на каждую
|
|
63
|
+
# правку списка, а это pandas и scikit-learn заново.
|
|
64
|
+
#
|
|
65
|
+
# Копируется ровно выбранный файл, а не вся папка: иначе правка чужого
|
|
66
|
+
# окружения сбрасывала бы кеш этому.
|
|
67
|
+
ARG KERNEL_ENV=base
|
|
68
|
+
COPY environments/${KERNEL_ENV}.txt /tmp/environment.txt
|
|
69
|
+
RUN pip install --no-cache-dir -r /tmp/environment.txt
|
|
70
|
+
|
|
71
|
+
# Student code runs as this user; it owns nothing outside /workspace.
|
|
72
|
+
#
|
|
73
|
+
# Идемпотентно: на втором слое цепочки runner уже заведён родителем, а второй
|
|
74
|
+
# `useradd` падает «already exists» и роняет сборку целиком.
|
|
75
|
+
RUN if ! id -u runner >/dev/null 2>&1; then useradd --create-home --uid 1000 runner; fi \
|
|
76
|
+
&& mkdir -p /workspace \
|
|
77
|
+
&& chown runner:runner /workspace
|
|
78
|
+
USER runner
|
|
79
|
+
WORKDIR /workspace
|
|
80
|
+
|
|
81
|
+
EXPOSE 8888
|
|
82
|
+
HEALTHCHECK --interval=5s --timeout=3s --start-period=20s --retries=12 \
|
|
83
|
+
CMD curl -fsS "http://localhost:8888/api/status?token=${JUPYTER_TOKEN}" || exit 1
|
|
84
|
+
|
|
85
|
+
CMD ["sh", "-c", "exec jupyter server \
|
|
86
|
+
--ServerApp.ip=0.0.0.0 \
|
|
87
|
+
--ServerApp.port=8888 \
|
|
88
|
+
--ServerApp.root_dir=/workspace \
|
|
89
|
+
--ServerApp.allow_remote_access=True \
|
|
90
|
+
--ServerApp.allow_origin='*' \
|
|
91
|
+
--ServerApp.disable_check_xsrf=True \
|
|
92
|
+
--IdentityProvider.token=\"$JUPYTER_TOKEN\" \
|
|
93
|
+
--no-browser"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Тяжёлая половина всех GPU-окружений: torch с CUDA, и больше ничего.
|
|
2
|
+
#
|
|
3
|
+
# colloq: gpu
|
|
4
|
+
#
|
|
5
|
+
# Строка выше — не комментарий, а директива: комната на этом окружении получает
|
|
6
|
+
# срез из KERNEL_GPUS на всё время жизни своего контейнера, а если свободных
|
|
7
|
+
# срезов нет — честно не открывается. Отката на процессор здесь нет и быть не
|
|
8
|
+
# может: колёса ниже собраны под CUDA. Признак наследуется, поэтому окружениям
|
|
9
|
+
# поверх этого повторять его не нужно.
|
|
10
|
+
#
|
|
11
|
+
# Отдельным окружением, а не строками в каждом GPU-списке, ровно по одной
|
|
12
|
+
# причине. Слой окружения один, и любая правка списка ставит его целиком
|
|
13
|
+
# заново: добавить timm или поднять версию transformers стоило трёх гигабайт
|
|
14
|
+
# колёс и девяти минут — каждый раз. Здесь они лежат один раз, а окружения
|
|
15
|
+
# сверху (`# colloq: from base-gpu`) собираются за секунды.
|
|
16
|
+
#
|
|
17
|
+
# Обычный индекс pypi, а не .../whl/cpu, как в cv: именно там колёса с CUDA.
|
|
18
|
+
# Драйвер приходит с хоста через nvidia-container-toolkit, ставить его в образ
|
|
19
|
+
# не надо. Первая сборка идёт долго, дальше образ лежит в кеше docker.
|
|
20
|
+
torch>=2.4
|
|
21
|
+
torchvision>=0.19
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Базовое окружение — то, что стоит в каждом семинаре и так.
|
|
2
|
+
#
|
|
3
|
+
# Здесь намеренно пусто. Ядро всегда ставит kernel/requirements.txt (jupyter,
|
|
4
|
+
# numpy, pandas, matplotlib, plotly, scikit-learn), а этот файл добавляется сверху.
|
|
5
|
+
# Пустой файл значит «ничего сверх базы» — и образ собирается за секунды.
|
|
6
|
+
#
|
|
7
|
+
# Чтобы завести своё окружение, не редактируйте этот файл:
|
|
8
|
+
#
|
|
9
|
+
# make env-new NAME=cv # создаст kernel/environments/cv.txt
|
|
10
|
+
# make env-use NAME=cv # соберёт образ и сделает его умолчанием
|
|
11
|
+
# # для НОВЫХ семинаров
|
|
12
|
+
seaborn
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Компьютерное зрение. Пример настоящего окружения — можно удалить.
|
|
2
|
+
#
|
|
3
|
+
# Ставится поверх базы, поэтому numpy/pandas/matplotlib перечислять не нужно.
|
|
4
|
+
# Сборка тянет около двух гигабайт: первый `make env-use NAME=cv` идёт долго,
|
|
5
|
+
# дальше образ лежит в кеше docker, и следующий семинар на нём заводится сразу.
|
|
6
|
+
# Открытых семинаров это не касается: окружение выбирают семинару при создании,
|
|
7
|
+
# и дальше оно не меняется.
|
|
8
|
+
|
|
9
|
+
# CPU-сборка: колёса для GPU весят втрое больше и на ноутбуке бесполезны.
|
|
10
|
+
--extra-index-url https://download.pytorch.org/whl/cpu
|
|
11
|
+
torch>=2.4
|
|
12
|
+
torchvision>=0.19
|
|
13
|
+
timm>=1.0
|
|
14
|
+
pillow>=10.3
|
|
15
|
+
opencv-python-headless>=4.10
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Нейросети на GPU. Образец окружения со срезом видеокарты — можно удалить.
|
|
2
|
+
#
|
|
3
|
+
# colloq: from base-gpu
|
|
4
|
+
#
|
|
5
|
+
# Строка выше — не комментарий, а директива: этот слой строится поверх образа
|
|
6
|
+
# base-gpu, где torch с CUDA уже стоит. Поэтому здесь только лёгкое сверху, и
|
|
7
|
+
# правка этого списка — секунды, а не девять минут на три гигабайта колёс.
|
|
8
|
+
#
|
|
9
|
+
# Срез видеокарты приходит оттуда же: base-gpu объявляет `# colloq: gpu`, и
|
|
10
|
+
# признак наследуется — комната на этом окружении получает срез из KERNEL_GPUS
|
|
11
|
+
# на всё время жизни своего контейнера, а если свободных нет, честно не
|
|
12
|
+
# открывается. Отката на процессор здесь нет: колёса собраны под CUDA.
|
|
13
|
+
#
|
|
14
|
+
# `make env-build NAME=gpu` первым делом соберёт base-gpu, если его образа ещё
|
|
15
|
+
# нет: это те самые девять минут — но один раз, а не на каждую правку.
|
|
16
|
+
transformers>=4.44
|
|
17
|
+
accelerate>=0.33
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Colloq: lightweight Kaggle-style CPU stack, verified without network.
|
|
2
|
+
# This is not the official Kaggle image.
|
|
3
|
+
# colloq: from base
|
|
4
|
+
numpy==2.4.6
|
|
5
|
+
pandas==3.0.6
|
|
6
|
+
scipy==1.17.1
|
|
7
|
+
scikit-learn==1.9.1
|
|
8
|
+
catboost==1.2.10
|
|
9
|
+
lightgbm==4.6.0
|
|
10
|
+
xgboost==3.2.0
|
|
11
|
+
optuna==4.9.0
|
|
12
|
+
polars==1.44.2
|
|
13
|
+
pyarrow==23.0.1
|
|
14
|
+
statsmodels==0.15.0
|
|
15
|
+
imbalanced-learn==0.14.2
|
|
16
|
+
category-encoders==2.11.1
|
|
17
|
+
shap==0.51.0
|
|
18
|
+
h3==4.5.0
|
|
19
|
+
nbclient==0.11.0
|
|
20
|
+
nbformat==5.11.1
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
jupyter-server>=2.14,<3
|
|
2
|
+
ipykernel>=6.29
|
|
3
|
+
numpy>=1.26
|
|
4
|
+
pandas>=2.2
|
|
5
|
+
matplotlib>=3.8
|
|
6
|
+
# Интерактивные графики. В базе, а не в окружении, по той же причине, что и
|
|
7
|
+
# matplotlib: `px.histogram(df, x=...)` пишут на первом же семинаре по данным, и
|
|
8
|
+
# «поставьте себе plotly» — не тот ответ, который даёт преподаватель классу
|
|
9
|
+
# посреди пары. Тетрадь рисует фигуру сама (shared/plotly.ts), и ядро для этого
|
|
10
|
+
# должно уметь её отдать.
|
|
11
|
+
#
|
|
12
|
+
# `narwhals` и `packaging` приезжают зависимостями plotly; pandas уже выше.
|
|
13
|
+
plotly>=6.0
|
|
14
|
+
scikit-learn>=1.4
|
|
15
|
+
requests>=2.32
|
|
16
|
+
# Форматирование ноутбука кнопкой FORMAT. Живёт в базе, а не в окружении:
|
|
17
|
+
# кнопка есть в каждой комнате, значит и инструмент должен быть везде.
|
|
18
|
+
black>=24.4
|
colloq/_app/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "colloq",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Colloq — собранное приложение. Зависимости ставит colloq при первом запуске.",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
9
|
+
"better-sqlite3": "^13.0.3",
|
|
10
|
+
"busboy": "^1.6.0",
|
|
11
|
+
"dotenv": "^16.4.7",
|
|
12
|
+
"express": "^4.21.2",
|
|
13
|
+
"lib0": "^0.2.99",
|
|
14
|
+
"openai": "^4.77.0",
|
|
15
|
+
"satori": "^0.33.4",
|
|
16
|
+
"ws": "^8.18.0",
|
|
17
|
+
"y-protocols": "^1.0.6",
|
|
18
|
+
"yjs": "^13.6.21"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Копия занятия на этой машине: база одним файлом и архив со всем, чего в базе
|
|
4
|
+
# нет. Пара к `colloq restore --legacy` (scripts/restore.sh).
|
|
5
|
+
#
|
|
6
|
+
# ./scripts/backup-local.sh снять копию в <состояние>/backups/
|
|
7
|
+
# colloq backup у преподавателя это он
|
|
8
|
+
# make backup-legacy в репозитории это он же, обёрткой в одну строку
|
|
9
|
+
#
|
|
10
|
+
# Почему отдельным файлом, а не рецептом в Makefile, где он прожил до сих пор.
|
|
11
|
+
# В колесо pip едут приложение и scripts/ — Makefile не едет и не поедет
|
|
12
|
+
# (scripts/pack.mts · SCRIPTS), а `colloq backup` у поставленного colloq звал
|
|
13
|
+
# make и умирал на «command not found» — на той единственной команде, которой
|
|
14
|
+
# преподаватель уносит семестр с машины. То же самое и по той же причине уже
|
|
15
|
+
# сделано с restore-legacy и с host: одно описание, один файл.
|
|
16
|
+
#
|
|
17
|
+
# ----------------------------------------------------------------- два корня
|
|
18
|
+
#
|
|
19
|
+
# Каталог ПРИЛОЖЕНИЯ — тот, что над scripts/: web/dist, kernel/, эти скрипты.
|
|
20
|
+
# Он приезжает с пакетом и сносится целиком следующим `pip install -U`.
|
|
21
|
+
# Каталог СОСТОЯНИЯ — .env, data/, workspace/, свои окружения, backups/ — живёт
|
|
22
|
+
# отдельно (~/.colloq или COLLOQ_HOME) и обновления переживает.
|
|
23
|
+
#
|
|
24
|
+
# Копия снимается ЦЕЛИКОМ с состояния, и адрес его здесь не вычисляется: его
|
|
25
|
+
# знает scripts/lib.sh (COLLOQ_STATE_ROOT), второго ответа на один вопрос быть
|
|
26
|
+
# не должно. В репозитории оба корня — один и тот же каталог, и там не сдвинулся
|
|
27
|
+
# ни один путь.
|
|
28
|
+
#
|
|
29
|
+
# Имена в архиве — тоже от корня состояния: развернуть его надо будет там же.
|
|
30
|
+
#
|
|
31
|
+
# ------------------------------------------------------------ что и почему
|
|
32
|
+
#
|
|
33
|
+
# Копия ЗДЕШНЕГО инстанса, и кладётся она в корень backups/. У среды на
|
|
34
|
+
# арендованной машине есть имя и свой подкаталог (backups/demo/ — туда пишет
|
|
35
|
+
# make vast-sync), а у этой машины имени нет: корень и есть её место. Так корень
|
|
36
|
+
# и подкаталоги не мешают друг другу — «последняя копия» одной среды никогда не
|
|
37
|
+
# окажется копией другой.
|
|
38
|
+
#
|
|
39
|
+
# VACUUM INTO читает согласованный снимок и пишет один готовый файл. Копия
|
|
40
|
+
# самого colloq.db этого не даёт: в режиме WAL половина дня лежит в журнале
|
|
41
|
+
# рядом, и файл, скопированный на ходу, отстаёт на часы. Проверено.
|
|
42
|
+
#
|
|
43
|
+
# Вторым файлом рядом — архив со всем, чего в базе нет: workspace (то, что
|
|
44
|
+
# загрузили в комнаты), файлы соревнований, wheel-наборы, ключ подписи и токен
|
|
45
|
+
# установки. Раньше README звал копировать папку руками, и это был ровно тот шаг, который пропускают: база
|
|
46
|
+
# без workspace — это тетради со ссылками на файлы, которых больше нет, а база
|
|
47
|
+
# без ключа подписи — инстанс, где все выданные ссылки мертвы. На арендованной
|
|
48
|
+
# машине, которую уничтожают после пары, цена такой забывчивости — весь семестр
|
|
49
|
+
# разом.
|
|
50
|
+
#
|
|
51
|
+
# Оба файла 0600 и не «файлы занятия»: в них лежат ключи от инстанса.
|
|
52
|
+
#
|
|
53
|
+
# В базе — сами занятия, преподаватели, история версий и настройки оракула.
|
|
54
|
+
# Слои Docker-образов не копируются. Закреплённый image ID в базе — ссылка,
|
|
55
|
+
# а не содержимое образа: для воспроизводимости старых посылок сохраните сами
|
|
56
|
+
# образы в registry либо отдельно через docker image save/load. Пересборка
|
|
57
|
+
# списка пакетов не гарантирует прежний image ID или состав окружения.
|
|
58
|
+
#
|
|
59
|
+
# ------------------------------------------------------- списки окружений
|
|
60
|
+
#
|
|
61
|
+
# Каталогов со списками пакетов на машине ДВА, и в копию идёт только второй:
|
|
62
|
+
#
|
|
63
|
+
# <приложение>/kernel/environments привезённые с продуктом (base, cv, gpu).
|
|
64
|
+
# Копировать нечего: они приедут снова с любой установкой пакета.
|
|
65
|
+
# <состояние>/environments заведённые человеком. Вот их и нет
|
|
66
|
+
# больше нигде — разбор, почему каталог отдельный, у ownEnvDir в
|
|
67
|
+
# cli/src/env.ts.
|
|
68
|
+
#
|
|
69
|
+
# В репозитории это ОДИН каталог, kernel/environments, и в архив он идёт
|
|
70
|
+
# целиком — ровно как раньше.
|
|
71
|
+
#
|
|
72
|
+
# Почему списки вообще в копии: их правят и заводят из панели прямо на той
|
|
73
|
+
# машине, где идут занятия (окно «Окружения» пишет <имя>.txt на хосте), и взять
|
|
74
|
+
# их больше неоткуда — в репозитории лежат только те, что приехали с ноутбука.
|
|
75
|
+
# Образы по ним пересобираются одной командой, а штампы `.<имя>.built` остаются
|
|
76
|
+
# машине: они про её образ, а не про список.
|
|
77
|
+
set -euo pipefail
|
|
78
|
+
|
|
79
|
+
cd "$(dirname "$0")/.."
|
|
80
|
+
|
|
81
|
+
RED=$'\033[31m'; DIM=$'\033[2m'; BOLD=$'\033[1m'; OFF=$'\033[0m'
|
|
82
|
+
say() { printf '%s\n' "$*"; }
|
|
83
|
+
die() { printf '%s%s%s\n' "$RED" "$*" "$OFF" >&2; exit 1; }
|
|
84
|
+
|
|
85
|
+
# Корень состояния — общий ответ на всех: scripts/lib.sh, тот же, что у host.sh
|
|
86
|
+
# и restore.sh.
|
|
87
|
+
. ./scripts/lib.sh
|
|
88
|
+
|
|
89
|
+
APP="$PWD"
|
|
90
|
+
STATE="$(cd "$COLLOQ_STATE_ROOT" 2>/dev/null && pwd || true)"
|
|
91
|
+
# Скобки у имени обязательны: следом идёт «»», а его первый байт bash считает
|
|
92
|
+
# частью имени переменной и падает на «unbound variable» вместо отказа по делу.
|
|
93
|
+
# Тем же приёмом и по той же причине пишет restore.sh.
|
|
94
|
+
[ -n "$STATE" ] || die "no state directory \"${COLLOQ_STATE_ROOT}\" — check COLLOQ_HOME."
|
|
95
|
+
|
|
96
|
+
# Свои окружения: в репозитории прежний kernel/environments, у поставленного
|
|
97
|
+
# пакета — <состояние>/environments (см. шапку).
|
|
98
|
+
if [ "$STATE" = "$APP" ]; then ENV_LISTS=kernel/environments; else ENV_LISTS=environments; fi
|
|
99
|
+
|
|
100
|
+
# Дальше всё считается от корня состояния — как раньше считалось от корня
|
|
101
|
+
# репозитория. Один переход вместо префикса в каждой строке: и имена в архиве
|
|
102
|
+
# получаются относительными сами собой.
|
|
103
|
+
cd "$STATE"
|
|
104
|
+
|
|
105
|
+
# Без базы копировать нечего, и сказать об этом надо вслух. VACUUM INTO над
|
|
106
|
+
# несуществующим файлом молча заводит пустую базу и так же молча пишет пустую
|
|
107
|
+
# «копию» — у поставленного colloq с разъехавшимися корнями это выглядело бы
|
|
108
|
+
# как удачная копия пустого места.
|
|
109
|
+
[ -f data/colloq.db ] || die "no data/colloq.db in $STATE — there is nothing to back up.
|
|
110
|
+
This is the directory that holds the .env of the class; for an installed colloq
|
|
111
|
+
its address is COLLOQ_HOME."
|
|
112
|
+
|
|
113
|
+
mkdir -p backups
|
|
114
|
+
stamp="$(date +%Y%m%d-%H%M%S)"
|
|
115
|
+
out="backups/colloq-$stamp.db"
|
|
116
|
+
files="backups/colloq-$stamp-files.tar.gz"
|
|
117
|
+
|
|
118
|
+
# Как назвать копию человеку. В репозитории — как и раньше, от корня: `backups/…`
|
|
119
|
+
# и есть путь от того каталога, в котором стоит человек. У поставленного colloq
|
|
120
|
+
# состояние лежит в ~/.colloq, а человек стоит где угодно, и относительное имя
|
|
121
|
+
# указывало бы на несуществующий backups/ рядом с ним — в том числе в подсказке
|
|
122
|
+
# «развернуть обратно», которую копируют целиком.
|
|
123
|
+
if [ "$STATE" = "$APP" ]; then SHOW=""; else SHOW="$STATE/"; fi
|
|
124
|
+
|
|
125
|
+
# Путь уходит внутрь строки SQL, а приходит он теперь снаружи (COLLOQ_HOME):
|
|
126
|
+
# одиночная кавычка в имени домашнего каталога закрыла бы строку посреди пути.
|
|
127
|
+
# В SQL её удваивают.
|
|
128
|
+
sql_out="$PWD/$out"
|
|
129
|
+
sqlite3 data/colloq.db "VACUUM INTO '${sql_out//\'/\'\'}'" \
|
|
130
|
+
|| die "did not work. sqlite3 is needed — brew install sqlite"
|
|
131
|
+
# Копия базы такая же секретная, как сама база: в ней ключи входа для
|
|
132
|
+
# преподавателей и ключ оракула, если его задавали в панели. VACUUM INTO
|
|
133
|
+
# создаёт файл с обычными правами, поэтому 0600 ставим сами.
|
|
134
|
+
chmod 600 "$out"
|
|
135
|
+
printf '%sbackup:%s %s %s(%s)%s\n' "$BOLD" "$OFF" "$SHOW$out" "$DIM" "$(du -h "$out" | cut -f1)" "$OFF"
|
|
136
|
+
|
|
137
|
+
back="colloq restore --legacy --db $SHOW$out"
|
|
138
|
+
set --
|
|
139
|
+
for p in workspace data/competitions data/dependencies data/session-secret data/setup-token "$ENV_LISTS"/*.txt; do
|
|
140
|
+
if [ -e "$p" ]; then set -- "$@" "$p"; fi
|
|
141
|
+
done
|
|
142
|
+
if [ $# -gt 0 ]; then
|
|
143
|
+
# Код tar разбирается, а не глотается: `set -e` иначе уронил бы скрипт на
|
|
144
|
+
# единице, которая здесь — не отказ (см. ниже).
|
|
145
|
+
code=0
|
|
146
|
+
tar -czf "$files" "$@" || code=$?
|
|
147
|
+
chmod 600 "$files" 2>/dev/null || true
|
|
148
|
+
[ "$code" -le 1 ] || die "could not take the class files — the backup is incomplete"
|
|
149
|
+
# Код 1 у tar это «файл менялся, пока его читали»: на ходу занятие пишет в
|
|
150
|
+
# workspace, и это нормально. Архив при этом годен, но об одном-двух файлах в
|
|
151
|
+
# нём знать стоит.
|
|
152
|
+
if [ "$code" -eq 1 ]; then
|
|
153
|
+
say "${DIM}some files changed while they were being copied — a class is running${OFF}"
|
|
154
|
+
fi
|
|
155
|
+
printf '%sfiles:%s %s %s(%s)%s\n' "$BOLD" "$OFF" "$SHOW$files" "$DIM" "$(du -h "$files" | cut -f1)" "$OFF"
|
|
156
|
+
# Оба файла названы в одной строке нарочно: разворачивать их надо парой, а
|
|
157
|
+
# искать вторую по имени первой человеку незачем.
|
|
158
|
+
back="$back --files $SHOW$files"
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
# Куда идти обратно. Прежняя подсказка звала `make restore`, а эта цель давно
|
|
162
|
+
# про переносимую копию k3s и на паре файлов отвечает «ARCHIVE is required»;
|
|
163
|
+
# make к тому же есть не у всех, кто эту копию снял. Пути — от корня состояния,
|
|
164
|
+
# то есть от того каталога, в котором они и лежат.
|
|
165
|
+
say "${DIM}restore it back: $back${OFF}"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Cluster application backup. MODE=live (default) or MODE=consistent.
|
|
3
|
+
# Consistent mode stops app, broker and every room; RESUME=1 explicitly starts
|
|
4
|
+
# the application afterwards. Failure leaves writers stopped for investigation.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
SCRIPT_PATH="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
|
|
7
|
+
cd "$(dirname "$0")/.."
|
|
8
|
+
umask 077
|
|
9
|
+
STATE="${COLLOQ_STATE_DIR:-/var/lib/colloq}"
|
|
10
|
+
MODE="${MODE:-live}"
|
|
11
|
+
NAME="${NAME:-}"
|
|
12
|
+
case "$MODE" in live|consistent) ;; *) echo 'MODE must be live or consistent' >&2; exit 1;; esac
|
|
13
|
+
case "$NAME" in *[!A-Za-z0-9-]*|-*|*-) echo 'invalid environment name' >&2; exit 1;; esac
|
|
14
|
+
RELEASE="${RELEASE:-$STATE/releases/current.json}"
|
|
15
|
+
[ -f "$RELEASE" ] || { echo 'No installed release. Local backup: colloq backup (make backup-legacy); cluster backup needs an installed release.' >&2; exit 1; }
|
|
16
|
+
OUT="${OUT:-backups${NAME:+/$NAME}/colloq-$(date -u +%Y%m%dT%H%M%SZ)-$MODE.tar.gz}"
|
|
17
|
+
mkdir -p "$STATE"
|
|
18
|
+
if ! python3 scripts/state-lock.py held --state "$STATE"; then
|
|
19
|
+
exec python3 scripts/state-lock.py run --state "$STATE" -- bash "$SCRIPT_PATH" "$@"
|
|
20
|
+
fi
|
|
21
|
+
[ ! -e "$STATE/.restore-in-progress" ] && [ ! -L "$STATE/.restore-in-progress" ] || { echo 'An interrupted restore must be recovered before backup.' >&2; exit 1; }
|
|
22
|
+
extra=()
|
|
23
|
+
if [ "$MODE" = consistent ]; then
|
|
24
|
+
bash scripts/cluster.sh stop
|
|
25
|
+
extra+=(--quiesced)
|
|
26
|
+
echo 'Consistent backup: all writers stopped; active kernel memory is discarded.' >&2
|
|
27
|
+
else
|
|
28
|
+
echo 'Live backup: SQLite snapshot is consistent; workspace files are copied at different instants.' >&2
|
|
29
|
+
fi
|
|
30
|
+
python3 scripts/runtime-backup.py backup --root "$STATE" --release "$RELEASE" --output "$OUT" --mode "$MODE" --name "$NAME" "${extra[@]}"
|
|
31
|
+
python3 scripts/runtime-backup.py validate --archive "$OUT" --name "$NAME" >&2
|
|
32
|
+
if [ "$MODE" = consistent ]; then
|
|
33
|
+
if [ "${RESUME:-}" = 1 ]; then bash scripts/cluster.sh start; else echo 'Writers remain stopped. Resume: bash scripts/cluster.sh start' >&2; fi
|
|
34
|
+
fi
|