Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/design/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,12 @@ through a named pipe with one server instance. It disconnects between clients wi
not replace that with disposal and recreation between requests: another process could claim ownership in the gap.

`BravoSingleInstance.ClaimOrRedirect` claims the pipe before the bootstrap: a secondary process forwards its startup
arguments and exits without creating directories, bootstrap services, a host or a window, and without reading the
user settings file. Keep the arbitration and the activation payload free of those steps: a secondary that builds
bootstrap services can transmit stored telemetry and reads files that the primary writes.
arguments and exits without creating directories, bootstrap services, a host or a window, without reading the user
settings file, and without running the `AppEnvironment` static constructor. Keep the arbitration and the activation
payload free of those steps: a secondary that builds bootstrap services can transmit stored telemetry and reads files
that the primary writes, and the `AppEnvironment` static constructor probes the WebView2 runtime, the deployment mode
and the registry. The payload code does not reference `AppEnvironment` and takes the Windows session from the
current process.

Current activation limits:

Expand Down
1 change: 0 additions & 1 deletion src/Bravo.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
<PackageReference Include="Nerdbank.GitVersioning" Version="3.10.85" PrivateAssets="all" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.0.0" PrivateAssets="all" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.Management" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
23 changes: 0 additions & 23 deletions src/Infrastructure/AppEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,6 @@ internal static class AppEnvironment
public static readonly string ApplicationRegistryApplicationTelemetryEnabledValue = "applicationTelemetryEnabled";
public static readonly string ApplicationRegistryApplicationTitleVersionHiddenValue = "applicationTitleVersionHidden";
public static readonly string ApplicationRegistryApplicationInstallFolderValue = "installFolder";
public static readonly string PBIDesktopProcessName = "PBIDesktop";
public static readonly string PBIDesktopSSASProcessImageName = "msmdsrv.exe";
public static readonly string[] PBIDesktopMainWindowTitleSuffixes = new string[]
{
// The PBIDesktop main window title is culture-specific.
// The suffix is not always present, for example, it is not added when the save/share function in OneDrive/SharePoint is active.

// Different dash characters are used as a separator
// See https://github.com/sql-bi/Bravo/issues/476
" \u002D Power BI Desktop", // Dash Punctuation - minus hyphen
" \u2013 Power BI Desktop", // Dash Punctuation - en dash
" \u2014 Power BI Desktop", // Dash Punctuation - em dash

// The whitespace character may not be present - Swedish/sv
// See https://github.com/sql-bi/Bravo/issues/510
"\u2013Power BI Desktop", // Dash Punctuation - en dash

// NBSP char instead of whitespace - Latvian/lv
"\u00A0\u2014 Power BI Desktop",
};
public static readonly Color ThemeColorDark = ColorTranslator.FromHtml("#202020");
public static readonly Color ThemeColorLight = ColorTranslator.FromHtml("#F3F3F3");
public static readonly DaxLineBreakStyle FormatDaxLineBreakDefault = DaxLineBreakStyle.InitialLineBreak;
Expand All @@ -77,7 +57,6 @@ static AppEnvironment()
_deploymentMode = new(() => GetDeploymentMode());
using var currentProcess = Process.GetCurrentProcess();

ProcessId = Environment.ProcessId;
SessionId = currentProcess.SessionId;

ApplicationDataPath = Path.Combine(Environment.GetFolderPath(DeploymentMode == AppDeploymentMode.Packaged ? Environment.SpecialFolder.UserProfile : Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.DoNotVerify), ApplicationName);
Expand All @@ -97,8 +76,6 @@ static AppEnvironment()
/// </summary>
public static int SessionId { get; }

public static int ProcessId { get; }

public static AppPublishMode PublishMode
{
get
Expand Down
86 changes: 86 additions & 0 deletions src/Infrastructure/Configuration/Settings/StartupCommandLine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Linq;

namespace Sqlbi.Bravo.Infrastructure.Configuration.Settings;

/// <summary>
/// The arguments Bravo was started with, as a Power BI Desktop External Tool passes them.
/// </summary>
internal sealed class StartupCommandLine
{
private StartupCommandLine()
{
}

public bool IsEmpty { get; private init; }

/// <summary>
/// Whether <c>--server</c> or <c>--database</c> was passed, even with a missing value.
/// </summary>
public bool HasConnectionArguments { get; private init; }

public string? ServerName { get; private init; }

public string? DatabaseName { get; private init; }

public int? ParentProcessId { get; private init; }

public IReadOnlyList<string> Errors { get; private init; } = [];

/// <param name="args">The arguments without the program name.</param>
public static StartupCommandLine Parse(IReadOnlyList<string> args)
{
ArgumentNullException.ThrowIfNull(args);

if (args.Count == 0)
return new StartupCommandLine { IsEmpty = true };

var serverOption = new Option<string>("--server");
serverOption.AddAlias("-s");
serverOption.IsRequired = true;
serverOption.Description = "Server name";
serverOption.Arity = ArgumentArity.ExactlyOne;

var databaseOption = new Option<string>("--database");
databaseOption.AddAlias("-d");
databaseOption.IsRequired = true;
databaseOption.Description = "Database name";
databaseOption.Arity = ArgumentArity.ExactlyOne;

var parentProcessIdOption = new Option<int>("--ppid");
parentProcessIdOption.IsRequired = false;
parentProcessIdOption.Description = "Parent process ID";
parentProcessIdOption.Arity = ArgumentArity.ExactlyOne;

var command = new RootCommand
{
serverOption,
databaseOption,
parentProcessIdOption
};

var parseResult = command.Parse(args.ToArray());

return new StartupCommandLine
{
HasConnectionArguments = parseResult.HasOption(serverOption) || parseResult.HasOption(databaseOption),
ServerName = HasValidValue(parseResult, serverOption) ? parseResult.GetValueForOption(serverOption) : null,
DatabaseName = HasValidValue(parseResult, databaseOption) ? parseResult.GetValueForOption(databaseOption) : null,
ParentProcessId = HasValidValue(parseResult, parentProcessIdOption) ? parseResult.GetValueForOption(parentProcessIdOption) : null,
Errors = parseResult.Errors.Select((e) => e.Message).ToArray(),
};
}

// GetValueForOption throws when the value is missing or cannot be converted, so it is read only without an error.
private static bool HasValidValue(ParseResult parseResult, Option option)
{
var optionResult = parseResult.FindResultFor(option);
if (optionResult is null)
return false;

return optionResult.ErrorMessage is null;
}
}
92 changes: 34 additions & 58 deletions src/Infrastructure/Configuration/Settings/StartupSettings.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using System;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Diagnostics;
using System.Linq;
using System.Text.Json.Serialization;
using Sqlbi.Bravo.Infrastructure.Extensions;
using Sqlbi.Bravo.Infrastructure.Helpers;
using Sqlbi.Bravo.Infrastructure.Services.PowerBI;

namespace Sqlbi.Bravo.Infrastructure.Configuration.Settings;

Expand All @@ -27,7 +26,7 @@ internal class StartupSettings
public string[]? CommandLineErrors { get; set; }

[JsonIgnore]
public bool IsPBIDesktopExternalTool => IsExternalTool && AppEnvironment.PBIDesktopProcessName.Equals(ParentProcessName, StringComparison.OrdinalIgnoreCase);
public bool IsPBIDesktopExternalTool => IsExternalTool && PowerBIDesktop.ProcessName.Equals(ParentProcessName, StringComparison.OrdinalIgnoreCase);

[JsonIgnore]
public int? ParentProcessId { get; set; }
Expand Down Expand Up @@ -60,70 +59,47 @@ public static void FromCommandLineArguments(this StartupSettings settings)
// If the file name is not available, the first element is equal to String.Empty.
// In.NET 5 and later versions, for single-file publishing, the first element is the name of the host executable.
var args = Environment.GetCommandLineArgs().Skip(1).ToArray();
if (args.Length == 0)
var commandLine = StartupCommandLine.Parse(args);

using var currentProcess = Process.GetCurrentProcess();

settings.FromCommandLine(commandLine, currentProcess);
}

// Does not run the AppEnvironment static constructor: a secondary instance builds its activation payload from these
// settings before it exits.
public static void FromCommandLine(this StartupSettings settings, StartupCommandLine commandLine, Process currentProcess)
{
if (commandLine.IsEmpty)
{
// No args provided
settings.IsEmpty = true;
return;
}

var serverOption = new Option<string>("--server");
serverOption.AddAlias("-s");
serverOption.IsRequired = true;
serverOption.Description = "Server name";
serverOption.Arity = ArgumentArity.ExactlyOne;

var databaseOption = new Option<string>("--database");
databaseOption.AddAlias("-d");
databaseOption.IsRequired = true;
databaseOption.Description = "Database name";
databaseOption.Arity = ArgumentArity.ExactlyOne;

var parentProcessIdOption = new Option<int>("--ppid");
parentProcessIdOption.IsRequired = false;
parentProcessIdOption.Description = "Parent process ID";
parentProcessIdOption.Arity = ArgumentArity.ExactlyOne;
settings.ArgumentServerName = commandLine.ServerName;
settings.ArgumentDatabaseName = commandLine.DatabaseName;
settings.IsExternalTool = commandLine.HasConnectionArguments;
settings.CommandLineErrors = commandLine.Errors.ToArray();

var command = new RootCommand
using var parentProcess = GetParentProcess(commandLine, currentProcess);
if (parentProcess is not null)
{
serverOption,
databaseOption,
parentProcessIdOption
};

var parseResult = command.Parse(args);

var serverOptionResult = parseResult.FindResultFor(serverOption);
if (serverOptionResult is not null && serverOptionResult.ErrorMessage is null)
settings.ArgumentServerName = parseResult.GetValueForOption(serverOption);
settings.ParentProcessId = parentProcess.Id;
settings.ParentProcessName = parentProcess.ProcessName;
//settings.ParentProcessMainWindowHandle = parentProcess.MainWindowHandle;
var windowTitle = parentProcess.GetMainWindowTitle();
settings.ParentProcessMainWindowTitle = settings.IsPBIDesktopExternalTool ? PowerBIDesktop.GetReportName(windowTitle) : windowTitle;
}
}

var databaseOptionResult = parseResult.FindResultFor(databaseOption);
if (databaseOptionResult is not null && databaseOptionResult.ErrorMessage is null)
settings.ArgumentDatabaseName = parseResult.GetValueForOption(databaseOption);
private static Process? GetParentProcess(StartupCommandLine commandLine, Process currentProcess)
{
if (commandLine.ParentProcessId is not int parentProcessId || !DesktopBridgeHelper.IsRunningAsMsixPackage())
return currentProcess.GetParentProcess();

settings.IsExternalTool = parseResult.HasOption(serverOption) || parseResult.HasOption(databaseOption);
settings.CommandLineErrors = parseResult.Errors.Select((e) => e.Message).ToArray();
if (!Process.TryGetById(parentProcessId, currentProcess.SessionId, out var parentProcess))
return null;

Process? parentProcess = null;
{
if (AppEnvironment.DeploymentMode == AppDeploymentMode.Packaged && parseResult.HasOption(parentProcessIdOption))
{
var parentProcessId = parseResult.GetValueForOption(parentProcessIdOption);
parentProcess = ProcessHelper.SafeGetProcessById(parentProcessId);
}
else
{
parentProcess = ProcessHelper.GetParentProcess();
}

if (parentProcess is not null)
{
settings.ParentProcessId = parentProcess.Id;
settings.ParentProcessName = parentProcess.ProcessName;
//settings.ParentProcessMainWindowHandle = parentProcess.MainWindowHandle;
settings.ParentProcessMainWindowTitle = settings.IsPBIDesktopExternalTool ? parentProcess.GetPBIDesktopMainWindowTitle() : parentProcess.GetMainWindowTitle();
}
}
parentProcess?.Dispose();
return parentProcess;
}
}
Loading
Loading