Public Access
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a763f4b113 | ||
|
|
93e35a0095 | ||
|
|
b80bf23341 | ||
|
|
8c58e5a558 |
@@ -0,0 +1,68 @@
|
||||
<Project>
|
||||
|
||||
<!--
|
||||
◆ THE TRIMMER'S VERSION IS PINNED HERE BECAUSE OTHERWISE THE LOCK FILES ARE NOT LOCKED.
|
||||
|
||||
Microsoft.NET.ILLink.Tasks is not referenced by anything in this repository. The SDK adds it
|
||||
on its own to any project that sets IsTrimmable or IsAotCompatible — DodoSSH.Contracts and
|
||||
DodoSSH.Crypto do, and the Android head gets it from trimming being on by default there — and
|
||||
the version it asks for is whatever the running SDK happens to bundle. That version lives in
|
||||
the SDK's own Microsoft.NETCoreSdk.BundledVersions.props, as a KnownILLinkPack item.
|
||||
|
||||
Which makes it a dependency whose version is a property of the toolchain rather than of this
|
||||
repository, and that is the whole problem: packages.lock.json records it as a Direct reference
|
||||
with a requested range, so the lock file silently means "whichever SDK last ran a restore".
|
||||
global.json says rollForward: latestMinor, so CI's setup-dotnet installs the newest 10.x SDK
|
||||
that exists on the day it runs. The moment .NET ships a servicing release, CI's SDK asks for a
|
||||
version the committed lock files do not have, and the locked-mode restore in ci.yml fails with
|
||||
NU1004 before a single file is compiled.
|
||||
|
||||
That is not hypothetical. It closed the whole pipeline: main's run 125 and every open pull
|
||||
request went red together, on
|
||||
|
||||
error NU1004: The package reference Microsoft.NET.ILLink.Tasks version has changed
|
||||
from [10.0.10, ) to [10.0.11, ).
|
||||
|
||||
with nothing in any of those commits touching a package. .NET had shipped SDK 10.0.400, which
|
||||
bundles ILLink 10.0.11 where 10.0.302 bundled 10.0.10, and setup-dotnet installed it the next
|
||||
time anything ran.
|
||||
|
||||
Worse than the outage is the shape of the repair without this pin. Regenerating the lock files
|
||||
holds only until the next servicing release, and it cannot be done from a machine whose newest
|
||||
SDK is older than the runner's: a restore on 10.0.302 writes 10.0.10 straight back and re-breaks
|
||||
CI, so the recorded version becomes a fact about whoever ran restore last rather than about this
|
||||
repository. That is exactly the state locking exists to prevent, and it is not a hypothetical
|
||||
either — every SDK installed on the machine this pin was written on tops out at 10.0.302.
|
||||
|
||||
Pinning it makes the recorded version a decision this repository made, reviewable in a diff
|
||||
like every other version in Directory.Packages.props, and identical on every machine whatever
|
||||
SDK it has. Moving it is then a deliberate edit here plus a regenerated lock file, which is the
|
||||
same ceremony any other dependency bump gets.
|
||||
|
||||
It is an Update on the SDK's item rather than a PackageVersion in Directory.Packages.props, and
|
||||
it has to be: the reference is implicit, so the SDK supplies the version itself and central
|
||||
package management never gets asked. ProcessFrameworkReferences reads @(KnownILLinkPack) when
|
||||
it runs, which is why this lives in Directory.Build.targets — the item does not exist yet while
|
||||
Directory.Build.props is being evaluated.
|
||||
|
||||
Keep this within a patch or two of the runtime the SDK ships. It is the trimming analyzer and
|
||||
the ILLink task, so a small skew is harmless, but a version far behind the framework being
|
||||
analysed is a real way to miss a trim warning.
|
||||
-->
|
||||
<Target Name="PinTheILLinkPackVersion" BeforeTargets="ProcessFrameworkReferences">
|
||||
<!--
|
||||
Inside a target, and not for tidiness. The SDK ships one KnownILLinkPack per target framework
|
||||
and they all share the identity "Microsoft.NET.ILLink.Tasks", so the TargetFramework metadata
|
||||
is the only thing telling net10.0's entry from net8.0's. A condition on %(...) is item
|
||||
batching, which MSBuild permits in a target and rejects during evaluation with MSB4191 — so
|
||||
an ItemGroup at the top of this file cannot express "only the net10.0 one" at all, and the
|
||||
unconditioned Update it would have to become rewrites every framework's entry.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<KnownILLinkPack Update="Microsoft.NET.ILLink.Tasks"
|
||||
Condition="'%(TargetFramework)' == 'net10.0'"
|
||||
ILLinkPackVersion="10.0.11" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -74,9 +74,9 @@
|
||||
},
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "f5VCIE7AJpd5YvzNTeMGVzQIgyE9tX+AreTYwQF+REbu+DZo/2Ae+jNSwhPEYrVz6RRkd7y8ubXjk6Nn6Ka+Cg=="
|
||||
"requested": "[10.0.11, )",
|
||||
"resolved": "10.0.11",
|
||||
"contentHash": "IBf7lbovvjGWVWXZX5cJ/cO0WXbId0Zq4BuSeT94mGZuOAP66oMeH9PTBZ9Jpp3Jb6jtK0qm/NyUbPRo1gC/wQ=="
|
||||
},
|
||||
"MinVer": {
|
||||
"type": "Direct",
|
||||
|
||||
@@ -84,14 +84,57 @@ const RELEASE_FOCUS_MESSAGE = 'dodossh.release-focus';
|
||||
const root = document.getElementById('root');
|
||||
const statusBanner = document.getElementById('status');
|
||||
|
||||
/** @type {Map<number, {term: object, fit: object, pane: HTMLElement}>} */
|
||||
/** @type {Map<number, {term: object, fit: object, pane: HTMLElement, notice: string}>} */
|
||||
const sessions = new Map();
|
||||
|
||||
/** @type {WebSocket | null} */
|
||||
let socket = null;
|
||||
|
||||
function setStatus(text) {
|
||||
statusBanner.textContent = text ?? '';
|
||||
/** Whose pane is showing, or null before there is one — see activate(). */
|
||||
let activeSessionId = null;
|
||||
|
||||
/*
|
||||
── THE BANNER BELONGS TO ONE PANE AT A TIME ─────────────────────────────────────────────────────────
|
||||
There is one #status element for the whole page, because there is one page for every terminal: the
|
||||
panes are stacked in the same box and all but the active one are hidden. What goes in it comes from
|
||||
two sources that are not the same size, and the difference is the whole of this.
|
||||
|
||||
The socket's troubles are the page's. There is a single socket behind every pane, so "the view is
|
||||
reconnecting" is true of whatever is on screen and true of the panes behind it.
|
||||
|
||||
A session's last words are not. "The remote closed the session." is a fact about one terminal and says
|
||||
nothing whatever about the others — so it is held on the session and drawn only while that session's
|
||||
pane is the one showing. Written straight into the shared element, which is what this used to do, it
|
||||
outlived the tab it described: switching to a live terminal left the dead one's epitaph sitting under
|
||||
it, and opening or closing any other tab wiped the message whether or not it belonged to that tab.
|
||||
|
||||
The socket's half wins when both have something to say: a page whose socket is down is not showing
|
||||
live output on any pane, which makes what became of one session the less urgent of the two.
|
||||
*/
|
||||
let transportStatus = statusBanner.textContent ?? '';
|
||||
|
||||
function renderStatus() {
|
||||
const notice = activeSessionId === null ? '' : sessions.get(activeSessionId)?.notice ?? '';
|
||||
|
||||
statusBanner.textContent = transportStatus || notice;
|
||||
}
|
||||
|
||||
/** Says something about the socket, which every pane shares. */
|
||||
function setTransportStatus(text) {
|
||||
transportStatus = text ?? '';
|
||||
renderStatus();
|
||||
}
|
||||
|
||||
/** Records what became of one session, to be drawn only while that session's pane is showing. */
|
||||
function setSessionNotice(sessionId, text) {
|
||||
const session = sessions.get(sessionId);
|
||||
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
session.notice = text ?? '';
|
||||
renderStatus();
|
||||
}
|
||||
|
||||
/** Builds a frame: opcode, big-endian session id, then payload. */
|
||||
@@ -292,7 +335,7 @@ function createSession(sessionId) {
|
||||
|
||||
term.onResize(() => sendResize(sessionId, term, pane));
|
||||
|
||||
const session = { term, fit, pane };
|
||||
const session = { term, fit, pane, notice: '' };
|
||||
sessions.set(sessionId, session);
|
||||
|
||||
activate(sessionId);
|
||||
@@ -306,6 +349,11 @@ function activate(sessionId) {
|
||||
session.pane.dataset.active = String(id === sessionId);
|
||||
}
|
||||
|
||||
// The banner follows the pane. Whatever this session has to say for itself replaces whatever the
|
||||
// session that was showing had to say for its own, which is the point of holding it per session.
|
||||
activeSessionId = sessionId;
|
||||
renderStatus();
|
||||
|
||||
const active = sessions.get(sessionId);
|
||||
if (active) {
|
||||
active.term.focus();
|
||||
@@ -373,7 +421,10 @@ function handleFrame(buffer) {
|
||||
session.term.write(REPLAY_BANNER);
|
||||
}
|
||||
|
||||
setStatus('');
|
||||
// This session's own line, and only this one's: a session that is open has nothing to say about
|
||||
// how it ended. The page's own "Connecting…" is cleared by the socket opening, which happens
|
||||
// before any frame can arrive.
|
||||
setSessionNotice(sessionId, '');
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -426,7 +477,14 @@ function handleFrame(buffer) {
|
||||
session.pane.remove();
|
||||
sessions.delete(sessionId);
|
||||
|
||||
setStatus('');
|
||||
// The notice went with the session record it was held on, but the page can still be pointing at
|
||||
// the pane that is now gone. Cleared rather than left dangling, so the banner stops describing a
|
||||
// closed tab while the host decides which pane to show next.
|
||||
if (activeSessionId === sessionId) {
|
||||
activeSessionId = null;
|
||||
}
|
||||
|
||||
renderStatus();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -503,14 +561,19 @@ function handleFrame(buffer) {
|
||||
const session = sessions.get(sessionId);
|
||||
const reason = new TextDecoder().decode(payload);
|
||||
|
||||
if (session) {
|
||||
// The pane and its scrollback stay. The user was probably reading the last thing the
|
||||
// remote said, and that is usually why the session ended.
|
||||
session.term.write(`\r\n\x1b[38;5;244m── ${reason} ──\x1b[0m\r\n`);
|
||||
session.term.options.cursorBlink = false;
|
||||
if (!session) {
|
||||
// No pane, so there is nothing this page can honestly hang the reason on. It used to go into
|
||||
// the banner anyway, which printed one session's ending underneath whichever pane happened to
|
||||
// be showing at the time.
|
||||
break;
|
||||
}
|
||||
|
||||
setStatus(reason);
|
||||
// The pane and its scrollback stay. The user was probably reading the last thing the
|
||||
// remote said, and that is usually why the session ended.
|
||||
session.term.write(`\r\n\x1b[38;5;244m── ${reason} ──\x1b[0m\r\n`);
|
||||
session.term.options.cursorBlink = false;
|
||||
|
||||
setSessionNotice(sessionId, reason);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -535,7 +598,7 @@ function scheduleReconnect() {
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('Reconnecting the terminal view…');
|
||||
setTransportStatus('Reconnecting the terminal view…');
|
||||
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null;
|
||||
@@ -555,7 +618,7 @@ function connect() {
|
||||
socket.binaryType = 'arraybuffer';
|
||||
|
||||
socket.addEventListener('open', () => {
|
||||
setStatus('');
|
||||
setTransportStatus('');
|
||||
|
||||
// Back to the quick attempt for whatever the next failure turns out to be. Kept slow between
|
||||
// attempts within one outage, reset once the outage is actually over.
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
},
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "f5VCIE7AJpd5YvzNTeMGVzQIgyE9tX+AreTYwQF+REbu+DZo/2Ae+jNSwhPEYrVz6RRkd7y8ubXjk6Nn6Ka+Cg=="
|
||||
"requested": "[10.0.11, )",
|
||||
"resolved": "10.0.11",
|
||||
"contentHash": "IBf7lbovvjGWVWXZX5cJ/cO0WXbId0Zq4BuSeT94mGZuOAP66oMeH9PTBZ9Jpp3Jb6jtK0qm/NyUbPRo1gC/wQ=="
|
||||
},
|
||||
"MinVer": {
|
||||
"type": "Direct",
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
},
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "f5VCIE7AJpd5YvzNTeMGVzQIgyE9tX+AreTYwQF+REbu+DZo/2Ae+jNSwhPEYrVz6RRkd7y8ubXjk6Nn6Ka+Cg=="
|
||||
"requested": "[10.0.11, )",
|
||||
"resolved": "10.0.11",
|
||||
"contentHash": "IBf7lbovvjGWVWXZX5cJ/cO0WXbId0Zq4BuSeT94mGZuOAP66oMeH9PTBZ9Jpp3Jb6jtK0qm/NyUbPRo1gC/wQ=="
|
||||
},
|
||||
"MinVer": {
|
||||
"type": "Direct",
|
||||
|
||||
Reference in New Issue
Block a user