max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+47 insertions,
-1 deletion
| @@ -34,7 +34,9 @@ | |||
| 34 | 34 | pty_requested: bool, | |
| 35 | 35 | /// TUI application handle for forwarding keypresses. | |
| 36 | 36 | app: Option<tui::AppHandle>, | |
| 37 | - | /// Channels stored between open and shell/subsystem request. | |
| 37 | + | /// Channels stored between open and shell/subsystem request. Only SFTP | |
| 38 | + | /// consumes one; see [`MnwHandler::release_channel`] for why every other | |
| 39 | + | /// path has to drop its entry rather than leave it parked here. | |
| 38 | 40 | channels: Arc<Mutex<HashMap<ChannelId, Channel<Msg>>>>, | |
| 39 | 41 | /// Active git subprocess stdin handles, keyed by channel. | |
| 40 | 42 | git_processes: HashMap<ChannelId, tokio::process::ChildStdin>, | |
| @@ -75,6 +77,25 @@ | |||
| 75 | 77 | pipe_uploads: HashMap::new(), | |
| 76 | 78 | } | |
| 77 | 79 | } | |
| 80 | + | ||
| 81 | + | /// Drop the parked [`Channel`] for a session we are going to drive through | |
| 82 | + | /// the [`Handle`](russh::server::Handle) and the handler callbacks instead. | |
| 83 | + | /// | |
| 84 | + | /// Not bookkeeping: it is what keeps a push from hanging. russh delivers | |
| 85 | + | /// every inbound `CHANNEL_DATA` twice, first by `send().await` onto the | |
| 86 | + | /// `Channel`'s bounded queue (`channel_buffer_size`, 100 messages) and only | |
| 87 | + | /// then to `Handler::data`. Holding a `Channel` nobody reads means that | |
| 88 | + | /// queue fills, the `send().await` blocks the session loop forever, and the | |
| 89 | + | /// handler stops being called at all. The client sits in `Writing objects` | |
| 90 | + | /// with a window the server already granted and never spends. | |
| 91 | + | /// | |
| 92 | + | /// It only bites above ~100 packets, which is why a small repo pushed fine | |
| 93 | + | /// and `shop` (4.5 MiB) stalled around 12%. Dropping the receiver makes | |
| 94 | + | /// russh's `send(...).unwrap_or(())` a no-op, and the channel keeps working | |
| 95 | + | /// through the handle. | |
| 96 | + | async fn release_channel(&self, channel: ChannelId) { | |
| 97 | + | self.channels.lock().await.remove(&channel); | |
| 98 | + | } | |
| 78 | 99 | } | |
| 79 | 100 | ||
| 80 | 101 | impl russh::server::Handler for MnwHandler { | |
| @@ -191,6 +212,10 @@ | |||
| 191 | 212 | channel: ChannelId, | |
| 192 | 213 | session: &mut Session, | |
| 193 | 214 | ) -> Result<(), Self::Error> { | |
| 215 | + | // The TUI writes through the session handle and reads keypresses from | |
| 216 | + | // `Handler::data`, so the parked channel would only queue up. | |
| 217 | + | self.release_channel(channel).await; | |
| 218 | + | ||
| 194 | 219 | let Some(ref user) = self.user else { | |
| 195 | 220 | tracing::warn!("shell_request without authenticated user"); | |
| 196 | 221 | session.close(channel)?; | |
| @@ -296,6 +321,12 @@ | |||
| 296 | 321 | let command_line = String::from_utf8_lossy(data); | |
| 297 | 322 | let handle = session.handle(); | |
| 298 | 323 | ||
| 324 | + | // Every exec path below (git, pipe upload, scp notice, plain command) | |
| 325 | + | // streams through `handle` and takes its stdin from `Handler::data`. | |
| 326 | + | // A push is the one that moves enough bytes to deadlock on the parked | |
| 327 | + | // channel's queue, but none of them read it. | |
| 328 | + | self.release_channel(channel).await; | |
| 329 | + | ||
| 299 | 330 | let Some(ref user) = self.user else { | |
| 300 | 331 | let _ = handle.close(channel).await; | |
| 301 | 332 | return Ok(()); | |
| @@ -547,6 +578,21 @@ | |||
| 547 | 578 | Ok(()) | |
| 548 | 579 | } | |
| 549 | 580 | ||
| 581 | + | async fn channel_close( | |
| 582 | + | &mut self, | |
| 583 | + | channel: ChannelId, | |
| 584 | + | _session: &mut Session, | |
| 585 | + | ) -> Result<(), Self::Error> { | |
| 586 | + | // A channel that was opened and then closed without a shell, exec or | |
| 587 | + | // subsystem request still has an entry parked in `channels`; a client | |
| 588 | + | // that closes without sending EOF still has a git child holding a pipe. | |
| 589 | + | // Both live for the length of the connection otherwise. | |
| 590 | + | self.release_channel(channel).await; | |
| 591 | + | self.git_processes.remove(&channel); | |
| 592 | + | self.pipe_uploads.remove(&channel); | |
| 593 | + | Ok(()) | |
| 594 | + | } | |
| 595 | + | ||
| 550 | 596 | async fn window_change_request( | |
| 551 | 597 | &mut self, | |
| 552 | 598 | _channel: ChannelId, |