sources/Google.Solutions.IapDesktop/Program.cs (590 lines of code) (raw):
//
// Copyright 2020 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
using Google.Apis.Util;
using Google.Solutions.Apis;
using Google.Solutions.Apis.Analytics;
using Google.Solutions.Apis.Auth;
using Google.Solutions.Apis.Auth.Gaia;
using Google.Solutions.Apis.Auth.Iam;
using Google.Solutions.Apis.Client;
using Google.Solutions.Apis.Compute;
using Google.Solutions.Apis.Crm;
using Google.Solutions.Apis.Logging;
using Google.Solutions.Common;
using Google.Solutions.Common.Diagnostics;
using Google.Solutions.Common.Threading;
using Google.Solutions.Common.Util;
using Google.Solutions.Iap;
using Google.Solutions.Iap.Net;
using Google.Solutions.IapDesktop.Application;
using Google.Solutions.IapDesktop.Application.Client;
using Google.Solutions.IapDesktop.Application.Diagnostics;
using Google.Solutions.IapDesktop.Application.Host;
using Google.Solutions.IapDesktop.Application.Profile;
using Google.Solutions.IapDesktop.Application.Profile.Auth;
using Google.Solutions.IapDesktop.Application.Profile.Settings;
using Google.Solutions.IapDesktop.Application.Theme;
using Google.Solutions.IapDesktop.Application.ToolWindows.ProjectExplorer;
using Google.Solutions.IapDesktop.Application.ToolWindows.Update;
using Google.Solutions.IapDesktop.Application.Windows;
using Google.Solutions.IapDesktop.Application.Windows.Auth;
using Google.Solutions.IapDesktop.Application.Windows.Dialog;
using Google.Solutions.IapDesktop.Application.Windows.Options;
using Google.Solutions.IapDesktop.Application.Windows.ProjectExplorer;
using Google.Solutions.IapDesktop.Application.Windows.ProjectPicker;
using Google.Solutions.IapDesktop.Core;
using Google.Solutions.IapDesktop.Core.ClientModel.Protocol;
using Google.Solutions.IapDesktop.Core.ClientModel.Transport;
using Google.Solutions.IapDesktop.Core.ObjectModel;
using Google.Solutions.IapDesktop.Core.ProjectModel;
using Google.Solutions.IapDesktop.Windows;
using Google.Solutions.Mvvm.Binding;
using Google.Solutions.Mvvm.Controls;
using Google.Solutions.Mvvm.Diagnostics;
using Google.Solutions.Mvvm.Theme;
using Google.Solutions.Platform;
using Google.Solutions.Platform.Dispatch;
using Google.Solutions.Platform.Net;
using Google.Solutions.Platform.Security;
using Google.Solutions.Platform.Security.Cryptography;
using Google.Solutions.Settings.Collection;
using Google.Solutions.Ssh;
using Google.Solutions.Terminal;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.WebSockets;
using System.Reflection;
using System.Security.Cryptography;
using System.Threading;
using System.Windows.Forms;
#pragma warning disable CA1031 // Do not catch general exception types
namespace Google.Solutions.IapDesktop
{
public class Program : SingletonApplicationBase
{
private static bool tracingEnabled = false;
private static readonly TraceSource[] TraceSources = new[]
{
ApiTraceSource.Log,
PlatformTraceSource.Log,
CommonTraceSource.Log,
IapTraceSource.Log,
SshTraceSource.Log,
TerminalTraceSource.Log,
ApplicationTraceSource.Log,
CoreTraceSource.Log,
};
public static string LogFile =>
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Google",
"IAP Desktop",
"Logs",
$"{DateTime.Now:yyyy-MM-dd_HHmm}.log");
public static bool IsLoggingEnabled
{
get
{
return tracingEnabled;
}
set
{
tracingEnabled = value;
if (tracingEnabled)
{
var logFilePath = LogFile;
Directory.CreateDirectory(new FileInfo(logFilePath).DirectoryName);
var logListener = new TextWriterTraceListener(logFilePath);
foreach (var trace in TraceSources)
{
trace.Listeners.Add(new DefaultTraceListener());
trace.Listeners.Add(logListener);
trace.Switch.Level = SourceLevels.Verbose;
}
}
else
{
foreach (var trace in TraceSources)
{
foreach (var listener in trace.Listeners.Cast<TraceListener>())
{
listener.Flush();
}
trace.Switch.Level = SourceLevels.Off;
}
}
}
}
private static IEnumerable<Assembly> LoadExtensionAssemblies()
{
var deprecatedExtensions = new[]
{
"google.solutions.iapdesktop.extensions.rdp.dll",
"google.solutions.iapdesktop.extensions.activity.dll",
"google.solutions.iapdesktop.extensions.os.dll",
"google.solutions.iapdesktop.extensions.shell.dll"
};
return Directory.GetFiles(
Path.GetDirectoryName(
Assembly.GetExecutingAssembly().Location),
"*.Extensions.*.dll")
.Where(dllPath => !deprecatedExtensions.Contains(new FileInfo(dllPath).Name.ToLower()))
.Select(dllPath => Assembly.LoadFrom(dllPath));
}
private static UserProfile LoadProfileOrExit(Install install, CommandLineOptions options)
{
try
{
return install.OpenProfile(options.Profile);
}
catch (Exception e)
{
MessageBox.Show(
e.Message,
"Profile",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
Environment.Exit(1);
throw new InvalidOperationException();
}
}
private static IAuthorization AuthorizeOrExit(IServiceProvider serviceProvider)
{
using (var dialog = serviceProvider
.GetService<WindowActivator<AuthorizeView, AuthorizeViewModel, IDialogTheme>>()
.CreateDialog())
{
//
// Initialize the view model.
//
dialog.ViewModel.DeviceEnrollment = DeviceEnrollment.Create(
new CertificateStore(),
serviceProvider.GetService<IRepository<IAccessSettings>>());
dialog.ViewModel.ClientRegistrations = new List<OidcClientRegistration>()
{
new OidcClientRegistration(
OidcIssuer.Gaia,
OAuthClient.Secrets.ClientId,
OAuthClient.Secrets.ClientSecret,
"/authorize/"),
new OidcClientRegistration(
OidcIssuer.Sts,
OAuthClient.SdkSecrets.ClientId,
OAuthClient.SdkSecrets.ClientSecret,
"/")
};
//
// Allow recovery from common errors.
//
dialog.ViewModel.OAuthScopeNotGranted += (_, retryArgs) =>
{
//
// User did not grant 'cloud-platform' scope.
//
using (var scopeDialog = serviceProvider
.GetService<WindowActivator<OAuthScopeNotGrantedView, OAuthScopeNotGrantedViewModel, IDialogTheme>>()
.CreateDialog())
{
retryArgs.Retry = scopeDialog.ShowDialog(dialog.ViewModel.View) == DialogResult.OK;
}
};
dialog.ViewModel.NetworkError += (_, retryArgs) =>
{
//
// This exception might be due to a missing/incorrect proxy
// configuration, so give the user a chance to change proxy
// settings.
//
var dialogParameters = new TaskDialogParameters(
"Authorization failed",
"IAP Desktop failed to complete the OAuth authorization. " +
"This might be due to network communication issues.",
retryArgs.Exception.Message)
{
Footnote = retryArgs.Exception.FullMessage(),
Icon = TaskDialogIcon.Error
};
dialogParameters.Buttons.Add(TaskDialogStandardButton.Cancel);
dialogParameters.Buttons.Add(new TaskDialogCommandLinkButton(
"Change network settings",
DialogResult.OK));
if (serviceProvider
.GetService<ITaskDialog>()
.ShowDialog(dialog.ViewModel.View, dialogParameters) == DialogResult.OK)
{
//
// Open settings.
//
retryArgs.Retry = OptionsDialog.Show(
dialog.ViewModel.View!,
(IServiceCategoryProvider)serviceProvider) == DialogResult.OK;
}
};
dialog.ViewModel.ShowOptions += (_, args) =>
{
using (var scopeDialog = serviceProvider
.GetService<WindowActivator<AuthorizeOptionsView, AuthorizeOptionsViewModel, IDialogTheme>>()
.CreateDialog())
{
scopeDialog.ShowDialog(dialog.ViewModel.View);
}
};
if (dialog.ShowDialog(null) == DialogResult.OK)
{
Debug.Assert(dialog.ViewModel.Authorization != null);
return dialog.ViewModel.Authorization!;
}
else
{
//
// User closed the dialog without completing the sign-in.
//
//
// Ensure logs are flushed.
//
IsLoggingEnabled = false;
Environment.Exit(1);
throw new InvalidOperationException();
}
}
}
//---------------------------------------------------------------------
// SingletonApplicationBase overrides.
//---------------------------------------------------------------------
private MainForm? initializedMainForm = null;
private readonly ManualResetEvent mainFormInitialized = new ManualResetEvent(false);
private readonly CommandLineOptions commandLineOptions;
internal Program(
string name,
CommandLineOptions commandLineOptions)
: base(name)
{
this.commandLineOptions = commandLineOptions;
}
protected override int HandleFirstInvocation(string[] args)
{
IsLoggingEnabled = this.commandLineOptions.IsLoggingEnabled;
//
// Set up process mitigations. This must be done early, otherwise it's
// ineffective.
//
try
{
ProcessMitigations.Apply();
}
catch (Exception e)
{
ApplicationTraceSource.Log.TraceError(e);
}
//
// Use the GDI-based TextRenderer class.
//
// NB. This must be set early, before the first window is created.
//
System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false);
System.Windows.Forms.Application.EnableVisualStyles();
#if DEBUG
ApplicationTraceSource.Log.Switch.Level = SourceLevels.Verbose;
TerminalTraceSource.Log.Switch.Level = SourceLevels.Verbose;
#endif
//
// Install patches requires for IAP.
//
try
{
SystemPatch.UnrestrictUserAgentHeader.Install();
}
catch (InvalidOperationException e)
{
ApplicationTraceSource.Log.TraceWarning(
"Installing UnrestrictUserAgentHeader patch failed: {0}", e);
}
try
{
WebSocket.RegisterPrefixes();
SystemPatch.SetUsernameAsHostHeaderForWssRequests.Install();
}
catch (Exception e)
{
ApplicationTraceSource.Log.TraceWarning(
"Installing SetUsernameAsHostHeaderForWssRequests patch failed: {0}", e);
}
//
// Set up layers. Services in a layer can lookup services in a lower layer,
// but not in a higher layer.
//
var preAuthLayer = new ServiceRegistry();
var install = new Install(Install.DefaultBaseKeyPath);
using (var profile = LoadProfileOrExit(install, this.commandLineOptions))
using (var processFactory = new Win32ChildProcessFactory(true))
{
Debug.Assert(!Install.IsExecutingTests);
//
// Load pre-auth layer: Platform abstractions, API adapters.
//
// We can only load and access services that don't require
// authorization. In particular, this means that we cannot access
// any Google APIs.
//
preAuthLayer.AddSingleton<IInstall>(install);
preAuthLayer.AddSingleton<UserAgent>(Install.UserAgent);
preAuthLayer.AddSingleton(profile);
preAuthLayer.AddSingleton<IUserProfile>(profile);
preAuthLayer.AddSingleton<IClock>(SystemClock.Default);
preAuthLayer.AddTransient<IConfirmationDialog, ConfirmationDialog>();
preAuthLayer.AddTransient<ITaskDialog, TaskDialog>();
preAuthLayer.AddTransient<ICredentialDialog, CredentialDialog>();
preAuthLayer.AddTransient<IInputDialog, InputDialog>();
preAuthLayer.AddTransient<IExceptionDialog, ExceptionDialog>();
preAuthLayer.AddTransient<IOperationProgressDialog, OperationProgressDialog>();
preAuthLayer.AddTransient<INotifyDialog, NotifyDialog>();
preAuthLayer.AddSingleton<IExternalRestClient, ExternalRestClient>();
preAuthLayer.AddTransient<HelpClient>();
preAuthLayer.AddTransient<BugReportClient>();
preAuthLayer.AddTransient<IHttpProxyAdapter, HttpProxyAdapter>();
preAuthLayer.AddSingleton<ProtocolRegistry>();
preAuthLayer.AddSingleton<IWin32ProcessFactory>(processFactory);
preAuthLayer.AddSingleton<IWin32ProcessSet>(processFactory);
preAuthLayer.AddSingleton<IKeyStore>(
new KeyStore(CngProvider.MicrosoftSoftwareKeyStorageProvider));
//
// Initialize UI, DPI mode.
//
var themeSettingsRepository = new ThemeSettingsRepository(
profile.SettingsKey.CreateSubKey("Theme"));
var dpiMode = (themeSettingsRepository.GetSettings().ScalingMode.Value) switch
{
ScalingMode.None => DpiAwarenessMode.DpiUnaware,
ScalingMode.LegacyGdi => DpiAwarenessMode.SystemAware,
_ => DpiAwarenessMode.SystemAware,
};
if (DpiAwareness.IsSupported && dpiMode != DpiAwarenessMode.DpiUnaware)
{
//
// Set DPI mode.
//
// NB. Setting the DPI mode programmatically (instead of using the
// app manifest or app.config) does causes WinForms to *not* deliver
// DPI change (WM_DPICHANGE) events, but that's ok.
//
try
{
DpiAwareness.ProcessMode = dpiMode;
}
catch (Exception e)
{
ApplicationTraceSource.Log.TraceWarning(
"Setting DPI mode to {0} failed: {1}", dpiMode, e.Message);
}
}
//
// Load settings.
//
var appSettingsRepository = new ApplicationSettingsRepository(profile);
preAuthLayer.AddSingleton<IRepository<IApplicationSettings>>(appSettingsRepository);
if (appSettingsRepository.IsPolicyPresent)
{
//
// If there are policies in place, mark the UA as
// Enterprise-managed.
//
Install.UserAgent.Extensions = "Enterprise";
}
var authSettingsRepository = new AuthSettingsRepository(
profile.SettingsKey.CreateSubKey("Auth"));
var accessSettingsRepository = new AccessSettingsRepository(
profile.SettingsKey.CreateSubKey("Application"),
profile.MachinePolicyKey?.OpenSubKey("Application"),
profile.UserPolicyKey?.OpenSubKey("Application"));
preAuthLayer.AddSingleton<IRepository<IAuthSettings>>(authSettingsRepository);
preAuthLayer.AddSingleton<IOidcOfflineCredentialStore>(authSettingsRepository);
preAuthLayer.AddSingleton<IRepository<IAccessSettings>>(accessSettingsRepository);
preAuthLayer.AddSingleton<IRepository<IThemeSettings>>(themeSettingsRepository);
preAuthLayer.AddSingleton(new ToolWindowStateRepository(
profile.SettingsKey.CreateSubKey("ToolWindows")));
preAuthLayer.AddSingleton<IBindingContext, ViewBindingContext>();
preAuthLayer.AddTransient<IBrowserProtocolRegistry, BrowserProtocolRegistry>();
//
// Load themes.
//
var themes = Themes.Load(themeSettingsRepository);
preAuthLayer.AddSingleton<ISystemDialogTheme>(themes.SystemDialog);
preAuthLayer.AddSingleton<IDialogTheme>(themes.Dialog);
preAuthLayer.AddSingleton<IToolWindowTheme>(themes.ToolWindow);
preAuthLayer.AddSingleton<IMainWindowTheme>(themes.MainWindow);
//
// Configure networking settings.
//
// NB. Until now, no network connections have been made.
//
var appSettings = appSettingsRepository.GetSettings();
//
// Override default set of TLS versions.
//
ServicePointManager.SecurityProtocol = appSettings.TlsVersions.Value;
try
{
//
// Activate proxy settings based on app settings.
//
preAuthLayer.GetService<IHttpProxyAdapter>().ActivateSettings(appSettings);
PscAndMtlsAwareHttpClientFactory.NtlmProxyAuthenticationRetries =
(ushort)appSettings.ProxyAuthenticationRetries.Value;
}
catch (Exception)
{
//
// Settings invalid -> ignore.
//
}
//
// Register and configure API client endpoints.
//
var serviceRoute = ServiceRoute.Public;
{
var accessSettings = accessSettingsRepository.GetSettings();
if (accessSettings.PrivateServiceConnectEndpoint.Value is var pscEndpoint &&
!string.IsNullOrEmpty(pscEndpoint))
{
//
// Enable PSC.
//
serviceRoute = new ServiceRoute(pscEndpoint);
}
//
// Set connection pool limit. This limit applies per endpoint.
//
ServicePointManager.DefaultConnectionLimit
= accessSettings.ConnectionLimit.Value;
}
preAuthLayer.AddSingleton(OAuthClient.ApiKey);
preAuthLayer.AddSingleton(serviceRoute);
preAuthLayer.AddSingleton(GaiaOidcClient.CreateEndpoint(serviceRoute));
preAuthLayer.AddSingleton(WorkforcePoolClient.CreateEndpoint(serviceRoute));
preAuthLayer.AddSingleton(ResourceManagerClient.CreateEndpoint(serviceRoute));
preAuthLayer.AddSingleton(ComputeEngineClient.CreateEndpoint(serviceRoute));
preAuthLayer.AddSingleton(OsLoginClient.CreateEndpoint(serviceRoute));
preAuthLayer.AddSingleton(LoggingClient.CreateEndpoint(serviceRoute));
preAuthLayer.AddSingleton(IapClient.CreateEndpoint(serviceRoute));
//
// Enable telemetry if the user allows it. Do this before
// authorization takes place.
//
TelemetryLog.Current = new AnalyticsLog(
new MeasurementClient(
MeasurementClient.CreateEndpoint(),
Install.UserAgent,
AnalyticsStream.ApiKey,
AnalyticsStream.MeasurementId),
install,
new Dictionary<string, object>()
{
{ DefaultParameters.UserAgent, Install.UserAgent.ToApplicationName() },
{ DefaultParameters.UserAgentArchitecture, ProcessEnvironment.ProcessArchitecture.ToString() },
{ DefaultParameters.UserAgentPlatformVersion, Environment.OSVersion.Version.ToString() },
{ "osdrk", SystemTheme.ShouldAppsUseDarkMode ? "1" : "0" },
{ "oscdp", DeviceCapabilities.Current.Dpi.ToString() },
{ "ossdp", DeviceCapabilities.System.Dpi.ToString() },
{ "apent", appSettingsRepository.IsPolicyPresent ? "1" : "0" },
{ "apsrt", serviceRoute == ServiceRoute.Public ? "Public" : "PSC" },
})
{
Enabled = appSettings.IsTelemetryEnabled.Value
};
preAuthLayer.AddSingleton(TelemetryLog.Current);
preAuthLayer.AddTransient<AuthorizeView>();
preAuthLayer.AddTransient<AuthorizeViewModel>();
preAuthLayer.AddTransient<AuthorizeOptionsView>();
preAuthLayer.AddTransient<AuthorizeOptionsViewModel>();
preAuthLayer.AddTransient<OAuthScopeNotGrantedView>();
preAuthLayer.AddTransient<OAuthScopeNotGrantedViewModel>();
preAuthLayer.AddTransient<PropertiesView>();
preAuthLayer.AddTransient<PropertiesViewModel>();
var authorization = AuthorizeOrExit(preAuthLayer);
//
// Authorization complete, now the main part of the application
// can be initialized.
//
// Load main layer, containing everything else (except for
// extensions).
//
var mainLayer = new ServiceRegistry(preAuthLayer);
mainLayer.AddSingleton<IAuthorization>(authorization);
mainLayer.AddTransient<IToolWindowHost, ToolWindowHost>();
var mainForm = new MainForm(mainLayer)
{
StartupUrl = this.commandLineOptions.StartupUrl,
ShowWhatsNew = this.commandLineOptions.IsPostInstall && install.PreviousVersion != null
};
mainLayer.AddSingleton<IJobHost>(mainForm);
//
// Load main services.
//
var eventService = new EventQueue(mainForm);
//
// Register API clients as singletons to ensure connection reuse.
//
mainLayer.AddSingleton<IResourceManagerClient, ResourceManagerClient>();
mainLayer.AddSingleton<IComputeEngineClient, ComputeEngineClient>();
mainLayer.AddSingleton<ILoggingClient, LoggingClient>();
mainLayer.AddSingleton<IOsLoginClient, OsLoginClient>();
mainLayer.AddSingleton<IIapClient, IapClient>();
mainLayer.AddSingleton<IReleaseFeed>(new GithubClient(
new ExternalRestClient(),
OAuthClient.RepositoryName));
mainLayer.AddTransient<IAddressResolver, AddressResolver>();
mainLayer.AddTransient<IWindowsCredentialGenerator, WindowsCredentialGenerator>();
mainLayer.AddSingleton<IJobService, JobService>();
mainLayer.AddSingleton<IEventQueue>(eventService);
mainLayer.AddSingleton<ISessionBroker, SessionBroker>();
mainLayer.AddSingleton<IBrowser>(Browser.Default);
var projectRepository = new ProjectRepository(profile.SettingsKey.CreateSubKey("Inventory"));
mainLayer.AddSingleton<IProjectRepository>(projectRepository);
mainLayer.AddSingleton<IProjectSettingsRepository>(projectRepository);
mainLayer.AddSingleton<IProjectWorkspace, ProjectWorkspace>();
mainLayer.AddTransient<ICloudConsoleClient, CloudConsoleClient>();
mainLayer.AddTransient<IUpdatePolicy, UpdatePolicy>();
mainLayer.AddSingleton<IIapTransportFactory, IapTransportFactory>();
mainLayer.AddSingleton<IDirectTransportFactory, DirectTransportFactory>();
//
// Load windows.
//
mainLayer.AddSingleton<IMainWindow>(mainForm);
mainLayer.AddSingleton<IWin32Window>(mainForm);
mainLayer.AddTransient<AccessInfoFlyoutView>();
mainLayer.AddTransient<AccessInfoViewModel>();
mainLayer.AddTransient<NewProfileView>();
mainLayer.AddTransient<NewProfileViewModel>();
mainLayer.AddTransient<IProjectPickerDialog, ProjectPickerDialog>();
mainLayer.AddTransient<ProjectPickerView>();
mainLayer.AddTransient<ProjectPickerViewModel>();
mainLayer.AddSingleton<IProjectExplorer, ProjectExplorer>();
mainLayer.AddSingleton<ProjectExplorerView>();
mainLayer.AddTransient<ProjectExplorerViewModel>();
mainLayer.AddTransient<ReleaseNotesView>();
mainLayer.AddTransient<ReleaseNotesViewModel>();
mainLayer.AddSingleton<UrlCommands>();
//
// Load extensions.
//
foreach (var extension in LoadExtensionAssemblies())
{
mainLayer.AddExtensionAssembly(extension);
}
//
// Run app.
//
this.initializedMainForm = mainForm;
this.initializedMainForm.Shown += (_, __) =>
{
//
// Form is now ready to handle subsequent invocations.
//
this.mainFormInitialized.Set();
};
this.initializedMainForm.FormClosing += (_, __) =>
{
//
// Stop handling subsequent invocations.
//
this.initializedMainForm = null;
};
using (new DebugMessageThrottle(TimeSpan.FromMilliseconds(100)))
using (var recorder = new MessageTraceRecorder(8))
{
//
// Replace the standard WinForms exception dialog.
//
System.Windows.Forms.Application.ThreadException += (_, exArgs)
=> ShowFatalErrorAndExit(exArgs.Exception, recorder.Capture());
mainForm.Shown += (_, __) =>
{
//
// Try to force the window into the foreground. This might
// not be allowed in all circumstances, but ensures that the
// window becomes visible after the user has completed a
// (browser-based) authorization.
//
TrySetForegroundWindow(Process.GetCurrentProcess().Id);
};
//
// Show the main window.
//
try
{
System.Windows.Forms.Application.Run(mainForm);
}
catch (Exception e)
{
ShowFatalErrorAndExit(e, recorder.Capture());
}
if (processFactory.ChildProcesses > 0)
{
//
// Instead of killing child processes outright, give
// them a chance to close gracefully (and possibly
// save any work).
//
try
{
WaitDialog.Wait(
null,
"Waiting for applications to close...",
async cancellationToken =>
{
//
// Give child processes a fixed time to close,
// but the user might cancel early.
//
using (var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
using (var combinedCts = timeoutCts.Token.Combine(cancellationToken))
{
await processFactory
.CloseAsync(combinedCts.Token)
.ConfigureAwait(true);
}
});
}
catch (Exception e)
{
#if DEBUG
if (!e.IsCancellation())
{
ShowFatalErrorAndExit(e, recorder.Capture());
}
#else
_ = e;
#endif
}
}
//
// Ensure logs are flushed.
//
IsLoggingEnabled = false;
return 0;
}
}
}
protected override int HandleSubsequentInvocation(string[] args)
{
var options = CommandLineOptions.ParseOrExit(args);
//
// Make sure the main form is ready. If that's not the case
// within a few seconds, then the process might be "stuck"
// in the sign-in process.
//
if (!this.mainFormInitialized.WaitOne(TimeSpan.FromSeconds(3)))
{
throw new TimeoutException("The main form is not ready");
}
//
// This method is called on the named pipe server thread - switch to
// main thread before doing any GUI stuff.
//
this.initializedMainForm?.Invoke(((Action)(() =>
{
if (options.StartupUrl != null)
{
this.initializedMainForm.ConnectToUrl(options.StartupUrl);
}
})));
return 1;
}
protected override void HandleSubsequentInvocationException(Exception e)
=> ShowFatalErrorAndExit(e, null);
private static void ShowFatalErrorAndExit(Exception e, MessageTrace? messageTrace)
{
//
// Ensure logs are flushed.
//
IsLoggingEnabled = false;
TelemetryLog.Current.Write(
"app_crash",
new Dictionary<string, object>
{
//
// Only include a base set of information that's
// safe to not contain any PII.
//
{ "error", e.GetType().Name },
{ "cause", e.InnerException?.GetType().Name ?? string.Empty },
{ "location", e.ToString(ExceptionFormatOptions.Compact) }
});
//
// NB. This could be called on any thread, at any time, so avoid
// touching the main form.
//
ErrorDialog.Show(new BugReport(typeof(Program), e)
{
WindowMessageTrace = messageTrace
});
Environment.Exit(e.HResult);
}
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
public static void Main(string[] args)
{
//
// Parse command line to catch errors before even passing an invalid
// command line to another instance of the app.
//
var options = CommandLineOptions.ParseOrExit(args);
try
{
var appName = "IapDesktop";
if (options.Profile != null)
{
//
// Incorporate the profile name (if provided) into the
// name of the singleton app so that instances can
// coexist if thex use different profiles.
//
appName += $"_{options.Profile}";
}
new Program(appName, options).Run(args);
}
catch (Exception e)
{
ShowFatalErrorAndExit(e, null);
}
}
internal static void LaunchNewInstance(CommandLineOptions options)
{
using (var process = new Process())
{
process.StartInfo = new ProcessStartInfo()
{
FileName = Assembly.GetExecutingAssembly().Location,
Arguments = options.ToString(),
WindowStyle = ProcessWindowStyle.Normal
};
process.Start();
}
}
}
}