Repository navigation
refactor: build the secondary activation payload without AppEnvironment - #985
Merged
albertospelta merged 9 commits intoOct 20, 2026
Merged
Conversation
The process name and the Analysis Services image name move to PowerBIDesktop as constants. A constant is compiled into its callers, so reading it no longer runs the AppEnvironment static constructor; this follows the split of AppEnvironment into small immutable types.
PowerBIDesktop.GetReportName removes the application name from a main window title. It replaces the GetPBIDesktopMainWindowTitle extension, which read the window and parsed its title in one step and returned a report name under the name of a title, so the parsing is now testable without a process. The suffixes become a private ImmutableArray instead of a public array field on AppEnvironment, which any caller could change. They are matched with an ordinal comparison: they are literal characters, not text to compare by culture. The test of the localized title formats moves from StringExtensionsTests and now checks the report name; new tests cover each separator.
Process.TryGetById and Process.GetParentProcess are extension members that take the session from the caller or from the process itself instead of AppEnvironment, so a caller that must not initialize AppEnvironment can use them. The WMI query of the parent lookup is unchanged. ProcessHelper.SafeGetProcessById and ProcessHelper.GetParentProcess keep their callers and delegate to them with the current session. A process found in another session, or one that has exited, is now disposed instead of being dropped with its handle open.
StartupCommandLine.Parse turns the arguments into values without reading the process environment, so the parsing is testable. FromCommandLine then finds the process that launched Bravo with the session of the current process, and tests the MSIX package directly instead of reading AppEnvironment.DeploymentMode. A secondary instance launched with arguments, for example as a Power BI Desktop External Tool, therefore builds its activation payload without running the AppEnvironment static constructor, as one launched without arguments already did. The payload is unchanged.
The test of the localized window title formats also finds the Microsoft Store package of Power BI Desktop, through the documented package APIs, since its WindowsApps folder cannot be listed. The resource assemblies are loaded from their bytes: files of a package folder can be read but not loaded as images.
process.GetParentProcess and GetChildrenPIDs read a Toolhelp32 process snapshot instead of querying WMI. The WMI query took about 200 ms, ran on a dedicated STA thread for each call and depended on the WMI service; a snapshot takes a few milliseconds and needs no service. A snapshot that cannot be taken throws, as a failed WMI query did, and is reported by the API boundary or the global handler. ProcessTree holds the snapshot and answers the parent and child queries without native calls, so they are unit tested. ProcessInterop takes the snapshot through CsWin32. PBIDesktopService takes one snapshot for all the Power BI Desktop processes it lists. Child processes are still limited to the session of their parent, now through Process.TryGetById, which also skips those that have exited. System.Management is no longer referenced.
A process records the identifier of its parent when it is created, and Windows reuses identifiers once a process exits. GetParentProcess and GetChildrenPIDs could therefore return an unrelated process that took the identifier of an exited parent or child. A parent must now have started before its child; a process whose start time cannot be read is treated as not found.
Aligns the name with the .NET naming guidelines for acronyms and compound words. No behavior change.
InteropGetParent, the NtQueryInformationProcess lookup kept as obsolete, ProcessHelper.GetParentProcess and GetParentProcessMainWindowHandle have no callers once process.GetParentProcess reads the Toolhelp32 snapshot, and AppEnvironment.ProcessId was read only by the WMI query. Ntdll goes with InteropGetParent, its only user.
albertospelta
deleted the
refactor/secondary-payload-without-appenvironment
branch
October 20, 2026 15:51
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A secondary instance launched with arguments, for example as a Power BI Desktop External Tool with
--serverand--database, now builds its activation payload without running theAppEnvironmentstatic constructor, as onelaunched without arguments already did. That constructor probes the WebView2 runtime, the deployment mode and the
registry, which a process that only forwards its arguments does not need.
Changes
PowerBIDesktop: the process and Analysis Services image names as constants, andGetReportName, which removes the application name from a main window title. The suffixes become a privateImmutableArray, matched ordinally.Processextension members that take the Windows session from the caller or the process:Process.TryGetByIdandprocess.GetParentProcess.StartupCommandLine.Parseturns the arguments into values;StartupSettingsresolves thelaunching process with the session of the current process and tests the MSIX package directly. An invalid
--ppidis reported as a command line error instead of throwing.
replaced by a Toolhelp32 snapshot taken through CsWin32 (
ProcessInterop) and read byProcessTree. Listing theopen reports takes one snapshot for all Power BI Desktop processes.
System.Managementis no longer referenced.child, compared in UTC.
GetChildrenPIDsis renamedGetChildProcessIds; parent lookups left without callers,NtdllandAppEnvironment.ProcessIdare removed.docs/design/architecture.mdstates that the payload code does notreference
AppEnvironment.Tests
ClaimOrRedirect_DoesNotReferenceAppEnvironmentwalks the IL reachable fromBravoSingleInstance.ClaimOrRedirect()and fails if any method references
AppEnvironment; a positive control checks the walker on a method that does.StartupCommandLine,ProcessTree,PowerBIDesktop.GetReportNameand the process extension members.resource files of 2.159 pass).
Verification
dotnet build Bravo.slnadds no warnings; 198 tests pass.launched with and without arguments delivered the same payload as
mainand never ran the static constructor; theprobe detects the constructor on
mainwith arguments.msmdsrv.exechild lookup used to list open reports, and the MSIX--ppidpath.