WAI dev and the relay close a WebSocket cleanly; Quantum Studio opens its course from a link #538
10 changed files with 88 additions and 17 deletions
|
|
@ -30,6 +30,9 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
|||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::{mpsc, watch};
|
||||
|
||||
/// How long a closed session's pipe is read on for the peer's end of it.
|
||||
const DRAIN: std::time::Duration = std::time::Duration::from_secs(2);
|
||||
|
||||
/// The sending end of a message pipe.
|
||||
#[async_trait]
|
||||
pub trait PipeTx: Send + 'static {
|
||||
|
|
@ -629,14 +632,19 @@ impl MuxConnection {
|
|||
},
|
||||
Ok(None) => {
|
||||
r.set_closed(TransportError::Closed { code: 0, reason: "the pipe closed".into(), by_peer: true });
|
||||
break;
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
r.set_closed(e);
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The session closed while the pipe is open: read on, discarding, until the peer ends
|
||||
// it (a WebSocket peer's Close) or for at most DRAIN. A socket shut with the peer's
|
||||
// bytes still unread is reset, not closed (a WebSocket closed this way never finishes
|
||||
// its closing handshake, and WebKit reports "Socket is not connected").
|
||||
let _ = tokio::time::timeout(DRAIN, async { while let Ok(Some(_)) = rx.recv().await {} }).await;
|
||||
});
|
||||
MuxConnection { inner, target: None }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ Ctrl+Enter (⌘+Enter on macOS) runs the program, and Escape cancels a run. The
|
|||
|
||||
None of these shows the solution. Progress (which lessons passed, how many checks and hints) is kept only in this browser, and the pane says so. *Quantum, reconsidered*, the course on interference and learning, is linked beside it.
|
||||
|
||||
A link can open it: `#course` in the page's address opens the pane, and `#course=<lesson>` opens it at that lesson (`#course=bell`); it goes with `#qasm=…`.
|
||||
|
||||
## Languages
|
||||
|
||||
The interface's words are data: `locales/<locale>/<area>.json`, one file for each part of the interface, English the source and Simplified Chinese beside it (`locales/index.js` gathers them). The Studio follows the browser's language until you pick one with the switch in the header; your choice is kept in this browser. A host may pin one with `data-lang` on the mount element. The engine's own messages (a verdict's statement, a diagnostic) stay as the engine writes them. The Chinese words are a machine-assisted translation, marked so in each file and in the footer, to be reviewed by a fluent reader. `node test/locales.test.mjs` holds every locale to every key, with the same placeholders.
|
||||
|
|
@ -264,6 +266,7 @@ The test runs the Studio in the installed browser:
|
|||
- editing by the drawing: a gate dropped from the palette onto a wire, one dragged to another wire, moved and deleted from the keyboard, each recorded as your edit, and a gate of a broadcast refused with its line;
|
||||
- the circuit described in the engine's words, and the drawing saved as SVG and as LaTeX;
|
||||
- the course, in English: a lesson's starter loaded and checked, what went wrong, a hint with its pointer, the solution passed and kept in the browser, a rule and a generator's cases each with their verdict;
|
||||
- a link to the course (`#course=bell`) opening the pane at that lesson;
|
||||
- the course in Chinese from the browser's language, the switch to English and back, and the layout at a phone's width with the course open;
|
||||
- the layout at a phone's width.
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ Ctrl+Enter(macOS 上为 ⌘+Enter)运行程序,Escape 取消运行。程
|
|||
|
||||
这些都不会显示答案。进度(哪些课已通过、检查和提示各用了多少次)只保存在这个浏览器中,窗格上也如此注明。旁边链接了 *Quantum, reconsidered*,一门关于干涉与学习的课程。
|
||||
|
||||
链接也可以打开它:页面地址中的 `#course` 打开课程窗格,`#course=<lesson>` 则打开到那一课(`#course=bell`);它可以与 `#qasm=…` 一起使用。
|
||||
|
||||
## 语言
|
||||
|
||||
界面上的文字是数据:`locales/<locale>/<area>.json`,界面的每个部分一个文件,英文为源文本,简体中文与之并列(由 `locales/index.js` 汇集)。在你用页眉中的切换开关选定一种语言之前,Studio 跟随浏览器的语言;你的选择保存在这个浏览器中。宿主页面可以在挂载元素上用 `data-lang` 固定一种语言。引擎自身的消息(判定的陈述、诊断)保持引擎写出时的原样。中文文字为机器辅助翻译,在每个文件和页脚中均已注明,尚待熟练的读者审校。`node test/locales.test.mjs` 要求每种语言都具备每一个键,且占位符相同。
|
||||
|
|
@ -234,6 +236,7 @@ node build.mjs --pkg PATH/TO/PKG && node test/studio.test.mjs
|
|||
- 通过线路图编辑:从门面板拖放到某条线上的门,拖到另一条线上的门,用键盘移动和删除的门,每一项都记录为你的编辑,以及广播中的一个门被拒绝并给出其所在行;
|
||||
- 用引擎的措辞描述的线路,以及保存为 SVG 和 LaTeX 的线路图;
|
||||
- 英文的课程:载入并检查一课的起始程序,哪里出了问题,附带引擎指引的提示,答案通过并保存在浏览器中,一条规则和一个生成器的各种情况各自附有判定;
|
||||
- 课程链接(`#course=bell`)打开到那一课;
|
||||
- 根据浏览器语言显示的中文课程,切换到英文再切换回来,以及打开课程时在手机宽度下的布局;
|
||||
- 手机宽度下的布局。
|
||||
|
||||
|
|
|
|||
|
|
@ -267,9 +267,15 @@ export function coursePane({ engine, program, load, ask, status }) {
|
|||
} catch (e) { say(e.message, 'bad'); }
|
||||
}
|
||||
button.addEventListener('click', () => open());
|
||||
let reopen = false;
|
||||
try { reopen = localStorage.getItem(OPEN) === '1'; } catch (e) {}
|
||||
if (reopen) open(true);
|
||||
// `#course` in the page's address opens the pane (a link from the course page), and
|
||||
// `#course=<lesson>` opens it at that lesson; otherwise it reopens as it was left.
|
||||
const linked = new URLSearchParams(location.hash.slice(1));
|
||||
let reopen = linked.has('course');
|
||||
if (!reopen) try { reopen = localStorage.getItem(OPEN) === '1'; } catch (e) {}
|
||||
if (reopen) open(true).then(() => {
|
||||
const id = linked.get('course');
|
||||
if (id && course?.units.some((u) => u.lessons.some((l) => l.id === id))) return show(id);
|
||||
});
|
||||
|
||||
return { node, button, open, show, check, store, current: () => lesson };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ const settles = (page, ...res) => page.waitForFunction((srcs) => {
|
|||
}, res.map((r) => r.source), { timeout: LONG }).then(() => true, () => false);
|
||||
const browser = await chromium.launch({ channel: process.env.WAI_E2E_CHANNEL ?? 'chrome', headless: true });
|
||||
|
||||
async function open(viewport = { width: 1400, height: 950 }, locale = 'en-US') {
|
||||
async function open(viewport = { width: 1400, height: 950 }, locale = 'en-US', path = '/') {
|
||||
const ctx = await browser.newContext({ viewport, colorScheme: 'dark', locale });
|
||||
const page = await ctx.newPage();
|
||||
const seen = { errors: [], failed: [] };
|
||||
|
|
@ -149,7 +149,7 @@ async function open(viewport = { width: 1400, height: 950 }, locale = 'en-US') {
|
|||
page.on('pageerror', (e) => seen.errors.push(String(e)));
|
||||
page.on('requestfailed', (r) => seen.failed.push(`${r.url()} (${r.failure()?.errorText})`));
|
||||
page.on('response', (r) => { if (r.status() >= 400) seen.failed.push(`${r.url()} (${r.status()})`); });
|
||||
await page.goto(ORIGIN + '/', { waitUntil: 'domcontentloaded' });
|
||||
await page.goto(ORIGIN + path, { waitUntil: 'domcontentloaded' });
|
||||
return { ctx, page, seen };
|
||||
}
|
||||
const done = (page) => page.waitForFunction(() => !/^Running|^On the service/.test(document.querySelector('.qs-status')?.textContent ?? '') && document.querySelector('#qs-run') && !document.querySelector('#qs-run').disabled, null, { timeout: LONG });
|
||||
|
|
@ -280,8 +280,11 @@ const before = (await page.$$('.qs-hitem')).length;
|
|||
await page.fill('#qs-shots', '321');
|
||||
await page.click('.qs-editor .cm-content');
|
||||
await page.keyboard.press('ControlOrMeta+Enter');
|
||||
// The run starts after the key, so its entry is waited for: the run button can still be
|
||||
// enabled, and the status not yet running, before the run has begun.
|
||||
const ranByKey = await page.waitForFunction((n) => document.querySelectorAll('.qs-hitem').length === n + 1, before, { timeout: LONG }).then(() => true, () => false);
|
||||
await done(page);
|
||||
check((await page.$$('.qs-hitem')).length === before + 1, 'Ctrl+Enter in the editor runs the program');
|
||||
check(ranByKey, 'Ctrl+Enter in the editor runs the program');
|
||||
const program = await page.evaluate(() => window.quantumStudio.view.state.doc.toString());
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
|
||||
|
|
@ -1189,6 +1192,14 @@ check(seen.failed.length === 0, `no failed request${seen.failed.length ? ': ' +
|
|||
await ctx.close();
|
||||
|
||||
// ---- the course ----
|
||||
// A link to it: `#course=<lesson>` opens the pane at that lesson.
|
||||
{
|
||||
const { ctx, page, seen } = await open(undefined, 'en-US', '/#course=bell');
|
||||
const at = await page.waitForFunction(() => !document.querySelector('#qs-course')?.hidden && document.querySelector('#qs-course-title')?.textContent === 'Two qubits, one answer', null, { timeout: LONG }).then(() => true, () => false);
|
||||
check(at && (await page.getAttribute('#qs-course-open', 'aria-expanded')) === 'true', `a link opens the course at its lesson (#course=bell: ${await page.textContent('#qs-course-title')})`);
|
||||
check(seen.errors.length === 0, `the course link: no console error${seen.errors.length ? ': ' + seen.errors.join(' | ') : ''}`);
|
||||
await ctx.close();
|
||||
}
|
||||
const COURSE = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'wai-quantum', 'course', 'lessons');
|
||||
const exercise = (id) => JSON.parse(readFileSync(join(COURSE, id, 'exercise.json'), 'utf8'));
|
||||
const lessonProgram = (v) => (Array.isArray(v) ? v.join('\n') + '\n' : v);
|
||||
|
|
|
|||
|
|
@ -5822,5 +5822,8 @@ Options:
|
|||
## Pages
|
||||
|
||||
- [The console](https://wai.transaction.science/quantum-run/): the service from a browser, or the same engine in the tab.
|
||||
- [Quantum Studio](https://wai.transaction.science/quantum-studio/): an editor that checks a program as it is typed and runs it in the tab or on the service.
|
||||
- [Quantum Studio](https://wai.transaction.science/quantum-studio/): an editor that checks a program as it is typed and runs it in the tab or on the service; its circuits course opens from `#course`.
|
||||
- [The quantum IDE](https://wai.transaction.science/quantum-ide/): build circuits in the browser tab and inspect them.
|
||||
- [Quantum algorithmic design](https://wai.transaction.science/quantum-learn/): a course on designing with interference on classical hardware, each lab run in the tab.
|
||||
- [WAI](https://wai.transaction.science/): the media transport and execution standard this quantum stack is part of.
|
||||
- [Source](https://git.transaction.science/Transaction-Science/open-standards): the engine, the command line and the service.
|
||||
|
|
|
|||
|
|
@ -137,7 +137,10 @@ Prompts:
|
|||
## Pages
|
||||
|
||||
- [The console](https://wai.transaction.science/quantum-run/): the service from a browser, or the same engine in the tab.
|
||||
- [Quantum Studio](https://wai.transaction.science/quantum-studio/): an editor that checks a program as it is typed and runs it in the tab or on the service.
|
||||
- [Quantum Studio](https://wai.transaction.science/quantum-studio/): an editor that checks a program as it is typed and runs it in the tab or on the service; its circuits course opens from `#course`.
|
||||
- [The quantum IDE](https://wai.transaction.science/quantum-ide/): build circuits in the browser tab and inspect them.
|
||||
- [Quantum algorithmic design](https://wai.transaction.science/quantum-learn/): a course on designing with interference on classical hardware, each lab run in the tab.
|
||||
- [WAI](https://wai.transaction.science/): the media transport and execution standard this quantum stack is part of.
|
||||
- [Source](https://git.transaction.science/Transaction-Science/open-standards): the engine, the command line and the service.
|
||||
|
||||
## Optional
|
||||
|
|
|
|||
|
|
@ -447,7 +447,10 @@ fn links() -> String {
|
|||
## Pages
|
||||
|
||||
- [The console]({SITE}/quantum-run/): the service from a browser, or the same engine in the tab.
|
||||
- [Quantum Studio]({SITE}/quantum-studio/): an editor that checks a program as it is typed and runs it in the tab or on the service.
|
||||
- [Quantum Studio]({SITE}/quantum-studio/): an editor that checks a program as it is typed and runs it in the tab or on the service; its circuits course opens from `#course`.
|
||||
- [The quantum IDE]({SITE}/quantum-ide/): build circuits in the browser tab and inspect them.
|
||||
- [Quantum algorithmic design]({SITE}/quantum-learn/): a course on designing with interference on classical hardware, each lab run in the tab.
|
||||
- [WAI]({SITE}/): the media transport and execution standard this quantum stack is part of.
|
||||
- [Source]({SOURCE}): the engine, the command line and the service.
|
||||
"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -543,11 +543,14 @@ mod tests {
|
|||
e(&mut forged);
|
||||
assert_eq!(verify(&forged)["valid"], false, "edit {k}");
|
||||
}
|
||||
// The same set runs to the same bytes, less the wall clock.
|
||||
// The same set runs to the same bytes, less the wall clock: each item's elapsed_ms,
|
||||
// the receipt over it, and its path, whose sibling leaves digest the other items'
|
||||
// results with their elapsed_ms.
|
||||
let again = run(&set, 1 << 30, &key(), json!({ "unmetered": "a test" })).unwrap();
|
||||
let strip = |b: &Value| {
|
||||
let mut b = b["result"]["items"].clone();
|
||||
for it in b.as_array_mut().unwrap() {
|
||||
it.as_object_mut().unwrap().remove("path");
|
||||
it["result"].as_object_mut().unwrap().remove("elapsed_ms");
|
||||
it["result"].as_object_mut().unwrap().remove("receipt");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -997,14 +997,42 @@ fn tunnel(mut c: TcpStream, h: &Head, rest: &[u8], to: SocketAddr) -> std::io::R
|
|||
}
|
||||
up.write_all(format!("{head}\r\n").as_bytes())?;
|
||||
up.write_all(rest)?;
|
||||
c.set_read_timeout(None)?;
|
||||
let (mut c2, mut up2) = (c.try_clone()?, up.try_clone()?);
|
||||
// Each side's end goes on as a half-close (a FIN), and the client's last bytes after the
|
||||
// relay's end are still read: a socket shut with the peer's bytes unread is reset, not
|
||||
// closed, so a WebSocket closed through the tunnel never finished its closing handshake
|
||||
// (WebKit reported "Socket is not connected"). Reads wake each second to see whether the
|
||||
// relay has gone, and the client gets 2 s after that to end its side.
|
||||
c.set_read_timeout(Some(Duration::from_secs(1)))?;
|
||||
let gone = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let (mut c2, mut up2, g) = (c.try_clone()?, up.try_clone()?, gone.clone());
|
||||
let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
|
||||
let t = std::thread::spawn(move || {
|
||||
let _ = std::io::copy(&mut up2, &mut c2);
|
||||
let _ = c2.shutdown(Shutdown::Both);
|
||||
g.store(true, std::sync::atomic::Ordering::Release);
|
||||
let _ = c2.shutdown(Shutdown::Write);
|
||||
let _ = done_tx.send(());
|
||||
});
|
||||
let _ = std::io::copy(&mut c, &mut up);
|
||||
let _ = up.shutdown(Shutdown::Both);
|
||||
let (mut buf, mut relay_open, mut since) = (vec![0u8; 16 * 1024], true, None::<Instant>);
|
||||
loop {
|
||||
match c.read(&mut buf) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
if relay_open && up.write_all(&buf[..n]).is_err() {
|
||||
relay_open = false;
|
||||
}
|
||||
}
|
||||
Err(e) if matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut | std::io::ErrorKind::Interrupted) => {}
|
||||
Err(_) => break,
|
||||
}
|
||||
if gone.load(std::sync::atomic::Ordering::Acquire) && since.get_or_insert_with(Instant::now).elapsed() >= Duration::from_secs(2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _ = up.shutdown(Shutdown::Write);
|
||||
// A relay that does not end its side once the client has gone is shut after 5 s.
|
||||
if done_rx.recv_timeout(Duration::from_secs(5)).is_err() {
|
||||
let _ = up.shutdown(Shutdown::Both);
|
||||
}
|
||||
let _ = t.join();
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue