Skip to content

feat(broker): stream live output and status from remote agent operations - #5254

Open
Vladyslav Nikonov (vnikonov-devolutions) wants to merge 4 commits into
vnikonov-devolutions-cautious-pancakefrom
vnikonov-devolutions-unigetui-event-channel-streaming
Open

feat(broker): stream live output and status from remote agent operations#5254
Vladyslav Nikonov (vnikonov-devolutions) wants to merge 4 commits into
vnikonov-devolutions-cautious-pancakefrom
vnikonov-devolutions-unigetui-event-channel-streaming

Conversation

@vnikonov-devolutions

Copy link
Copy Markdown
Contributor

Brokered (remote agent) package operations now show live process output while they run. When an operation is routed through the Devolutions Agent broker, UniGetUI opens the broker's new per-operation event channel and streams the remote process's stdout/stderr straight into the operation output pane, line by line, exactly like a locally executed operation — including progress lines.

What this changes for users:

  • Live output: brokered installs/updates/uninstalls display the package manager's output as it happens, instead of showing no output at all.
  • Instant status updates: status changes (Starting → Running → Completed/Failed/Canceled) are picked up as soon as the broker signals them, rather than waiting for the next poll.
  • Restored result parsing: since output is captured again, package managers can parse it to produce accurate operation results (e.g. detecting specific failure conditions in the output text).
  • Graceful fallback: if the broker does not advertise an event channel, or the channel cannot be opened or breaks mid-stream, the operation transparently falls back to the previous status-polling behavior — the operation itself is never failed because live streaming was unavailable.
  • Cancellation: canceling a streamed operation still requests a broker-side cancel; the tail of the process output is preserved while the cancellation is confirmed.

Stacked on #5253 (Devolutions.Now.Policy 2026.8.5 bump + remote cancellation); only the last commit belongs to this PR.

Issue: DGW-438

@vnikonov-devolutions

Copy link
Copy Markdown
Contributor Author

Implementation notes

Event-channel consumption (src/UniGetUI.PackageEngine.Operations/PackageOperations.cs):

  • After Execute, if ExecutionResponse.Operation.EventChannel is present, the channel is opened with BrokerClient.OpenEventChannel and consumed via OperationEventChannel.ReadEvents (Devolutions.Now.Policy 2026.8.5 — already referenced by feat(broker): support cancelling remote agent operations #5253, so no package bump was needed).
  • Frame handling: Stdout/Stderr → incremental Line(...) calls (stderr as LineType.Error); StatusUpdatedQueryStatus (status transitions logged once); StdoutOverflow/StderrOverflow → warning line with the number of skipped bytes; Finish → immediate final QueryStatus (no poll delay) feeding the existing terminal-status interpretation.
  • Line buffering mirrors the local process reader (AbstractProcessOperation): frame payloads may split lines arbitrarily, so a per-stream buffer reassembles them; LF-terminated text is emitted as a regular line, CR-terminated text (progress bars) as LineType.ProgressIndicator, promoted to a regular line on a following bare LF.

Fallback matrix — streaming problems never fail the operation:

Condition Behavior
EventChannel is null (broker without support) status polling, info log
OpenEventChannel throws (connect failure/timeout) warning + status polling
Fatal mid-stream error (EventFrameException, IOException — corrupt frames, unsupported Hello major, truncated stream) flush buffered output, warning + status polling

Cancel while streaming: the cancel request path from #5253 is reused (RequestBrokerCancel, bounded by the request timeout); afterwards the channel is drained under the confirm timeout so the output tail and the Finish frame are honored, then the terminal status is confirmed over the status endpoint (poll-based confirm as fallback if the drain fails). The remote-process-wins race (Completed/Failed before cancel takes effect) is honored as before.

Output-based veredicts restored: InterpretBrokerTerminalStatus now feeds accumulated Error/Information lines to GetProcessVeredict — the same filter the local process path applies — so manager result parsers see the streamed output text again.

Tests (src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs):

  • EventChannelPipeServer: a real NamedPipeServerStream writing hand-encoded NOW_BROKER frames (u32 body_size LE | u16 kind LE | body), mirroring the approach of the library's own OperationEventChannelTests.
  • ScriptedBrokerTransport extended with EventChannelPipeName (advertises a LocalPipe channel in the execution response) and OnCancelRequested (test synchronization).
  • 5 new tests: streamed stdout/stderr reaches both the operation output and the manager result parser (incl. cross-frame partial-line reassembly, overflow warning, and exactly one status query after FINISH); STATUS_UPDATED triggers a status query; missing event channel falls back to polling; unsupported Hello major version (fatal decode error) falls back to polling and the operation still succeeds; cancel during streaming sends the broker cancel, preserves the post-cancel output tail, and yields Canceled.

Validation: full dotnet test UniGetUI.Windows.slnx /p:Platform=x64 suite green (all projects; PackageOperationsTests 29/29 on both net10.0 and net10.0-windows). NativeAOT/trim-safe: no reflection or reflection-based JSON on production paths (tests use the library's source-generated BrokerJson).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds support for consuming the broker’s per-operation event channel to stream live stdout/stderr and status hints during brokered operations, with robust fallback to status polling.

Changes:

  • Implement event-channel streaming in brokered operations (stdout/stderr + status hints), with fallback to polling on failure.
  • Refactor broker cancel flow to separate “request cancel” vs “confirm terminal status”, and drain the event stream during cancellation when possible.
  • Add end-to-end tests using a named-pipe event-channel server and scripted broker transport.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/UniGetUI.PackageEngine.Operations/PackageOperations.cs Adds event-channel streaming, streaming output buffering, and updated cancel/status logic.
src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs Adds tests + test pipe server to validate streaming, fallback, and cancellation behaviors.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/UniGetUI.PackageEngine.Operations/PackageOperations.cs Outdated
Comment thread src/UniGetUI.PackageEngine.Operations/PackageOperations.cs Outdated
Comment thread src/UniGetUI.PackageEngine.Operations/PackageOperations.cs
@vnikonov-devolutions
Vladyslav Nikonov (vnikonov-devolutions) force-pushed the vnikonov-devolutions-unigetui-event-channel-streaming branch from 85df3b2 to 55a5f54 Compare August 6, 2026 19:15
…hannel

When the broker advertises a per-operation event channel (LocalPipe) in the
execution response, consume it for live output: stdout/stderr frames are
emitted line-by-line into the operation log (mirroring the local process
reader, including CR progress lines), STATUS_UPDATED triggers a status query,
overflow frames log a skipped-bytes warning, and FINISH ends streaming with a
final status query. Accumulated output is fed back to the manager's result
parser, restoring output-based veredicts for brokered operations.

If the channel is absent, fails to open, or breaks mid-stream (fatal decode
error / transport failure), the operation falls back to the existing HTTP
status polling without failing. Cancellation during streaming requests a
broker-side cancel, drains the channel (bounded) so the output tail and
Finish frame are honored, then confirms the terminal status; the poll-based
cancel flow is kept for the non-streaming path.

Tests cover streamed output reaching both the operation log and the result
parser (including partial-line reassembly across frames), STATUS_UPDATED and
FINISH handling, fallback on missing channel and on decode errors, and
cancellation during streaming, using a real named-pipe frame server.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…, span scanning

- Result parsers now receive only raw process output streamed over the event
  channel (tracked in a dedicated list) instead of scraping the operation log,
  keeping internal informational lines out of the parser input.
- StreamedOutputLineBuffer clears the pending CR progress line whenever a
  regular line is emitted, so superseded progress text (foo\rbar\n) is no
  longer promoted by a later bare LF.
- Append() now scans with IndexOfAny over spans instead of per-character
  iteration.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@vnikonov-devolutions
Vladyslav Nikonov (vnikonov-devolutions) force-pushed the vnikonov-devolutions-unigetui-event-channel-streaming branch from 55a5f54 to 421df5f Compare August 7, 2026 09:50
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…cution

Brokered operations previously requested Standard elevation unless the user
explicitly checked 'Run as administrator': WinGet's auto-detection of
elevation-requiring installers (ElevationRequired/ElevatesSelf metadata,
System-scope installs) only ran inside _getOperationParameters, which the
broker path never calls. As a result, packages like FreeCAD ran non-elevated
through the agent and the installer raised its own UAC prompt.

- Add IPackageOperationHelper.ApplyElevationRequirements with a no-op default
  in BasePkgOperationHelper, so any manager can opt into elevation detection.
- WinGet overrides it with the existing detection logic, factored out of
  _getOperationParameters (which still invokes it at the same point, keeping
  local-path behavior identical, including the ElevationProhibited checks).
- PerformBrokerOperation now calls the hook before CreateBrokerClient, so
  OverridenOptions.RunAsAdministrator feeds RequiresAdminRights() the same way
  as the local path; Settings.K.ProhibitElevation still wins.
- Log the requested elevation (Elevated/Standard) in the operation output for
  diagnosability, next to the effective user.
- Tests: capture the /v1/package-operations/execute wire body in the scripted
  transport and assert Client.RequestedElevation; cover helper-forced Elevated,
  default Standard, and ProhibitElevation override.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants