Skip to content

Commit

Permalink
Installer for the plugin.
Browse files Browse the repository at this point in the history
Issue #134
pre-martin committed Sep 11, 2024
1 parent cde972a commit ea469e8
Showing 18 changed files with 620 additions and 2 deletions.
4 changes: 2 additions & 2 deletions README.adoc
Original file line number Diff line number Diff line change
@@ -47,13 +47,13 @@ image::images/teaser/Teaser-2.png[Teaser 2,800]

== Installation

WARNING: To download, do not use the green button! Instead, click on "Releases" on the right side and download the file with extension `streamDeckPlugin`.
WARNING: To download, do not use the green button! Instead, click on "Releases" on the right side and download the installer file.

Be sure to have the SimHub Property Server plugin installed into SimHub (see above). When updating this plugin, be sure to also check the SimHub Property Server plugin for updates.

TIP: For the usage of this plugin, the https://dotnet.microsoft.com/en-us/download/dotnet/8.0[.NET Runtime 8.0] has to be installed. Without this, the plugin won't even start. Download ".NET Desktop Runtime 8.0.x (x64)" from Microsoft.

Download the file `net.planetrenner.simhub.streamDeckPlugin` from the GitHub "Releases" page and double-click it to install it into Stream Deck.
Download the installer from the GitHub "Releases" page and double-click it to install it into Stream Deck.


== Usage
67 changes: 67 additions & 0 deletions StreamDeckSimHub.Installer/Actions/AbstractInstallerAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (C) 2024 Martin Renner
// LGPL-3.0-or-later (see file COPYING and COPYING.LESSER)

using System.Windows.Media;
using CommunityToolkit.Mvvm.ComponentModel;

namespace StreamDeckSimHub.Installer.Actions;

public abstract partial class AbstractInstallerAction : ObservableObject, IInstallerAction
{
private readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();

public abstract string Name { get; }

[ObservableProperty]
private string _message = string.Empty;

[ObservableProperty]
private Brush _actionState = ActionStates.InactiveBrush;

public async Task<ActionResult> Execute()
{
_logger.Info($"Starting action {GetType().Name}");

ActionState = ActionStates.RunningBrush;
try
{
var result = await ExecuteInternal();
ActionState = result switch
{
ActionResult.Success => ActionStates.SuccessBrush,
ActionResult.Error => ActionStates.ErrorBrush,
ActionResult.NotRequired => ActionStates.InactiveBrush,
_ => ActionState
};

_logger.Info($"Finished action {GetType().Name} with result {result}");
await Task.Delay(1000);
return result;
}
catch (Exception e)
{
SetAndLogError(e, $"Action {GetType().Name} failed with exception");
return ActionResult.Error;
}
}

protected abstract Task<ActionResult> ExecuteInternal();

protected void SetAndLogInfo(string message)
{
Message = message;
_logger.Info(message);
}

protected void SetAndLogError(string message)
{
Message = message;
_logger.Info(message);
}

protected void SetAndLogError(Exception e, string message)
{
Message = message;
_logger.Error(e, message);
}
}
29 changes: 29 additions & 0 deletions StreamDeckSimHub.Installer/Actions/IInstallerAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (C) 2024 Martin Renner
// LGPL-3.0-or-later (see file COPYING and COPYING.LESSER)

using System.Windows.Media;

namespace StreamDeckSimHub.Installer.Actions;

public enum ActionResult
{
Success,
NotRequired,
Error,
}

public abstract class ActionStates
{
public static readonly Brush InactiveBrush = Brushes.Gray;
public static readonly Brush RunningBrush = Brushes.Orange;
public static readonly Brush SuccessBrush = Brushes.Green;
public static readonly Brush ErrorBrush = Brushes.Red;
}

public interface IInstallerAction
{
string Name { get; }
string Message { get; }
Brush ActionState { get; }
Task<ActionResult> Execute();
}
88 changes: 88 additions & 0 deletions StreamDeckSimHub.Installer/Actions/InstallStreamDeckPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (C) 2024 Martin Renner
// LGPL-3.0-or-later (see file COPYING and COPYING.LESSER)

using System.IO;
using System.IO.Compression;
using StreamDeckSimHub.Installer.Tools;

namespace StreamDeckSimHub.Installer.Actions;

public class InstallStreamDeckPlugin : AbstractInstallerAction
{
public override string Name => "Installing Stream Deck SimHub Plugin";

protected override Task<ActionResult> ExecuteInternal()
{
if (Directory.Exists(Path.Combine(Configuration.StreamDeckPluginDir, Configuration.PluginDirName)))
{
SetAndLogInfo("Deleting existing Stream Deck SimHub Plugin");
var result = DeleteExistingInstallation(Path.Combine(Configuration.StreamDeckPluginDir, Configuration.PluginDirName));
if (!result)
{
return Task.FromResult(ActionResult.Error);
}
}

SetAndLogInfo("Installing Stream Deck SimHub Plugin");
if (!ExtractPlugin(Configuration.StreamDeckPluginDir))
{
return Task.FromResult(ActionResult.Error);
}

SetAndLogInfo("Successfully installed Stream Deck SimHub Plugin");
return Task.FromResult(ActionResult.Success);
}

private bool DeleteExistingInstallation(string pluginDir)
{
try
{
// Delete all files in the base directory
var baseDirInfo = new DirectoryInfo(pluginDir);
foreach (var fileInfo in baseDirInfo.EnumerateFiles())
{
fileInfo.Delete();
}

// Delete all directories recursive in the base directory - except "images"
foreach (var dirInfo in baseDirInfo.EnumerateDirectories().Where(dirInfo => dirInfo.Name != "images"))
{
dirInfo.Delete(true);
}

// Delete all directories recursive in the "images" directory - except "custom"
var imagesDirInfo = new DirectoryInfo(Path.Combine(pluginDir, "images"));
foreach (var dirInfo in imagesDirInfo.EnumerateDirectories().Where(dirInfo => dirInfo.Name != "custom"))
{
dirInfo.Delete(true);
}

return true;
}
catch (Exception e)
{
SetAndLogError(e, $"Could not delete existing installation: {e.Message}");
return false;
}
}

private bool ExtractPlugin(string streamDeckPluginDir)
{
var myAssembly = typeof(App).Assembly;
var resourcePaths = myAssembly.GetManifestResourceNames().Where(res => res.EndsWith(Configuration.PluginZipName)).ToList();
if (resourcePaths.Count == 1)
{
using var pluginStream = myAssembly.GetManifestResourceStream(resourcePaths[0]);
if (pluginStream == null)
{
SetAndLogError("Could not find embedded Stream Deck SimHub Plugin (stream is null)");
return false;
}
ZipFile.ExtractToDirectory(pluginStream, streamDeckPluginDir, true);
return true;
}

SetAndLogError($"Could not find embedded Stream Deck SimHub Plugin ({resourcePaths.Count} streams)");
return false;
}
}
38 changes: 38 additions & 0 deletions StreamDeckSimHub.Installer/Actions/StartStreamDeckSoftware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (C) 2024 Martin Renner
// LGPL-3.0-or-later (see file COPYING and COPYING.LESSER)

using System.IO;
using Microsoft.Win32;
using StreamDeckSimHub.Installer.Tools;

namespace StreamDeckSimHub.Installer.Actions;

/// <summary>
/// Starts the Stream Deck software.
/// </summary>
public class StartStreamDeckSoftware : AbstractInstallerAction
{
public override string Name => "Starting Stream Deck software";

protected override Task<ActionResult> ExecuteInternal()
{
var installDir = GetStreamDeckInstallationPath();
ProcessTools.StartProcess(Path.Combine(installDir, "StreamDeck.exe"), installDir);

SetAndLogInfo("Stream Deck software started");
return Task.FromResult(ActionResult.Success);
}

private string GetStreamDeckInstallationPath()
{
var installPath = (string?) Registry.GetValue(Configuration.StreamDeckRegistryFolder, "InstallDir", null);
if (!string.IsNullOrEmpty(installPath))
{
SetAndLogInfo($"Found Stream Deck directory in registry: {installPath}");
return installPath;
}

SetAndLogInfo($"Could not find Stream Deck directory in registry. Using default.");
return Path.Combine("C:", "Program Files", "Elgato", "StreamDeck");
}
}
78 changes: 78 additions & 0 deletions StreamDeckSimHub.Installer/Actions/StopStreamDeckSoftware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (C) 2024 Martin Renner
// LGPL-3.0-or-later (see file COPYING and COPYING.LESSER)

using StreamDeckSimHub.Installer.Tools;

namespace StreamDeckSimHub.Installer.Actions;

/// <summary>
/// Stops the Stream Deck software, if it is running.
/// </summary>
public partial class StopStreamDeckSoftware : AbstractInstallerAction
{
public override string Name => "Stopping Stream Deck software";

protected override async Task<ActionResult> ExecuteInternal()
{
if (!IsStreamDeckRunning())
{
SetAndLogInfo("Stream Deck software is not running. Stopping not required.");
return ActionResult.NotRequired;
}

if (!IsPluginRunning())
{
SetAndLogInfo("Plugin is not running.");
}

var process = ProcessTools.GetProcess(Configuration.StreamDeckProcessName);
process?.Kill(true);

if (!await WaitForStreamDeckKilled())
{
SetAndLogError("The Stream Deck software could not be stopped. Please stop it manually and try again.");
return ActionResult.Error;
}

if (!await WaitForPluginKilled())
{
SetAndLogError("The Stream Deck SimHub Plugin could not be stopped. Please kill it manually and try again.");
return ActionResult.Error;
}

SetAndLogInfo("The Stream Deck software stopped.");
return ActionResult.Success;
}

private bool IsStreamDeckRunning()
{
return ProcessTools.IsProcessRunning(Configuration.StreamDeckProcessName);
}

private bool IsPluginRunning()
{
return ProcessTools.IsProcessRunning(Configuration.PluginProcessName);
}

private async Task<bool> WaitForStreamDeckKilled()
{
for (var i = 0; i < 10; i++)
{
if (!IsStreamDeckRunning()) return true;
await Task.Delay(1000);
}

return false;
}

private async Task<bool> WaitForPluginKilled()
{
for (var i = 0; i < 10; i++)
{
if (!IsPluginRunning()) return true;
await Task.Delay(1000);
}

return false;
}
}
10 changes: 10 additions & 0 deletions StreamDeckSimHub.Installer/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Application x:Class="StreamDeckSimHub.Installer.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:StreamDeckSimHub.Installer"
StartupUri="MainWindow.xaml"
Startup="App_OnStartup">
<Application.Resources>

</Application.Resources>
</Application>
19 changes: 19 additions & 0 deletions StreamDeckSimHub.Installer/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (C) 2024 Martin Renner
// LGPL-3.0-or-later (see file COPYING and COPYING.LESSER)

using System.Reflection;
using System.Windows;
using NLog;

namespace StreamDeckSimHub.Installer;

/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
private void App_OnStartup(object sender, StartupEventArgs e)
{
LogManager.Setup().LoadConfigurationFromAssemblyResource(typeof(App).GetTypeInfo().Assembly);
}
}
10 changes: 10 additions & 0 deletions StreamDeckSimHub.Installer/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Windows;

[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
72 changes: 72 additions & 0 deletions StreamDeckSimHub.Installer/MainWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<Window x:Class="StreamDeckSimHub.Installer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:StreamDeckSimHub.Installer"
xmlns:localActions="clr-namespace:StreamDeckSimHub.Installer.Actions"
mc:Ignorable="d"
Title="Stream Deck SimHub Plugin Installer" MinHeight="500" Width="600" SizeToContent="Height">

<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>

<Grid Margin="10,5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>

<TextBlock Grid.Row="0" FontSize="14" TextWrapping="WrapWithOverflow">
<Span FontWeight="SemiBold" FontSize="16">This program will install the Stream Deck SimHub Plugin into the Stream Deck software.</Span>
<LineBreak />
<Span>Version:</Span> <Run Text="{Binding Version, Mode=OneWay}" />
<LineBreak />
<LineBreak />
<Span>Your custom images will be preserved.</Span>
<LineBreak />
<LineBreak />
<Span>Run this program with your current user (i.e. do not run as administrator if you are currently not logged in as administrator).</Span>
</TextBlock>

<Border Grid.Row="1" BorderThickness="1" BorderBrush="DarkGray" Margin="0, 20">
<StackPanel Orientation="Vertical">
<ItemsControl ItemsSource="{Binding InstallerSteps}">
<d:ItemsControl.ItemsSource>
<x:Array Type="{x:Type localActions:IInstallerAction}">
<localActions:StopStreamDeckSoftware Message="Stream Deck software is not running. Stopping not required." />
<localActions:StopStreamDeckSoftware Message="Stream Deck software is not running. Stopping not required." />
<localActions:InstallStreamDeckPlugin Message="Some message" />
</x:Array>
</d:ItemsControl.ItemsSource>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical" Margin="0,0,0,5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="20" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0" FontWeight="DemiBold" Content="{Binding Path=Name}" />
<Ellipse Grid.Column="1" Width="10" Height="10" Fill="{Binding Path=ActionState}" Stroke="Black" />
</Grid>
<TextBlock Text="{Binding Path=Message}" Margin="10,0,20,0" TextWrapping="WrapWithOverflow" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>

<TextBlock Grid.Row="2" FontSize="14" TextWrapping="WrapWithOverflow" MinHeight="50" Foreground="{Binding ResultBrush}">
<Run Text="{Binding Result, Mode=OneWay}" />
</TextBlock>

<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,25,0,5">
<Button FontWeight="SemiBold" FontSize="16" Padding="15,5" Command="{Binding InstallCommand}">Install</Button>
</StackPanel>
</Grid>
</Window>
17 changes: 17 additions & 0 deletions StreamDeckSimHub.Installer/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (C) 2024 Martin Renner
// LGPL-3.0-or-later (see file COPYING and COPYING.LESSER)

using System.Windows;

namespace StreamDeckSimHub.Installer;

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
76 changes: 76 additions & 0 deletions StreamDeckSimHub.Installer/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (C) 2024 Martin Renner
// LGPL-3.0-or-later (see file COPYING and COPYING.LESSER)

using System.Collections.ObjectModel;
using System.Windows.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using StreamDeckSimHub.Installer.Actions;

namespace StreamDeckSimHub.Installer;

public partial class MainWindowViewModel : ObservableObject
{
private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger();
private static readonly Brush SuccessBrush = Brushes.Green;
private static readonly Brush ErrorBrush = Brushes.Red;

public string Version => ThisAssembly.AssemblyFileVersion;

[ObservableProperty]
private ObservableCollection<IInstallerAction> _installerSteps = [];

[ObservableProperty]
private string _result = string.Empty;

[ObservableProperty]
private Brush _resultBrush = SuccessBrush;

[RelayCommand]
private async Task Install()
{
ClearResultText();
InstallerSteps.Clear();
Logger.Info("========== Starting installation ==========");

var stopStreamDeck = new StopStreamDeckSoftware();
InstallerSteps.Add(stopStreamDeck);
if (await stopStreamDeck.Execute() == ActionResult.Error)
{
SetErrorResultText();
return;
}

var result = true;
var installStreamDeckPlugin = new InstallStreamDeckPlugin();
InstallerSteps.Add(installStreamDeckPlugin);
result &= await installStreamDeckPlugin.Execute() != ActionResult.Error;

var startStreamDeck = new StartStreamDeckSoftware();
InstallerSteps.Add(startStreamDeck);
result &= await startStreamDeck.Execute() != ActionResult.Error;

if (result) SetSuccessResultText();
else SetErrorResultText();
}

private void ClearResultText()
{
Result = string.Empty;
ResultBrush = SuccessBrush;
}

private void SetSuccessResultText()
{
Result = "The plugin was installed successfully. You can exit the program now.";
ResultBrush = SuccessBrush;
}

private void SetErrorResultText()
{
Result = """
The installation was NOT successful. Please stop the Stream Deck software manually and try again.
""";
ResultBrush = ErrorBrush;
}
}
15 changes: 15 additions & 0 deletions StreamDeckSimHub.Installer/NLog.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
throwConfigExceptions="true">

<targets>
<target xsi:type="File" name="logfile" fileName="installer.log"
deleteOldFileOnStartup="true"
layout="${longdate} - ${level:padding=-5} - ${message} ${exception:format=tostring}" />
</targets>

<rules>
<logger name="*" minlevel="Debug" writeTo="logfile" />
</rules>
</nlog>
6 changes: 6 additions & 0 deletions StreamDeckSimHub.Installer/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
= Installer

. Build the plugin: `release.bat` or `release.bat debug`

. Build the installer: `dotnet publish StreamDeckSimHub.Installer\StreamDeckSimHub.Installer.csproj`

29 changes: 29 additions & 0 deletions StreamDeckSimHub.Installer/StreamDeckSimHub.Installer.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>false</SelfContained>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>embedded</DebugType>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="NLog" Version="5.3.3" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="NLog.config" />
<EmbeddedResource Include="..\build\net.planetrenner.simhub.streamDeckPlugin" />
</ItemGroup>
</Project>
20 changes: 20 additions & 0 deletions StreamDeckSimHub.Installer/Tools/Configuration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (C) 2024 Martin Renner
// LGPL-3.0-or-later (see file COPYING and COPYING.LESSER)

using System.IO;

namespace StreamDeckSimHub.Installer.Tools;

public static class Configuration
{
public const string StreamDeckProcessName = "StreamDeck";
public const string PluginProcessName = "StreamDeckSimHub";

public const string StreamDeckRegistryFolder = @"HKEY_CURRENT_USER\SOFTWARE\Elgato Systems GmbH\StreamDeck";

public static readonly string AppDataRoaming = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
public static readonly string StreamDeckPluginDir = Path.Combine(AppDataRoaming, "Elgato", "StreamDeck", "Plugins");

public const string PluginDirName = "net.planetrenner.simhub.sdPlugin";
public const string PluginZipName = "net.planetrenner.simhub.streamDeckPlugin";
}
38 changes: 38 additions & 0 deletions StreamDeckSimHub.Installer/Tools/ProcessTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (C) 2024 Martin Renner
// LGPL-3.0-or-later (see file COPYING and COPYING.LESSER)

using System.Diagnostics;
using System.IO;

namespace StreamDeckSimHub.Installer.Tools;

public static class ProcessTools
{
/// <summary>
/// Is a given process running?
/// </summary>
public static bool IsProcessRunning(string processName)
{
return GetProcess(processName) != null;
}

/// <summary>
/// Simple wrapper for <c>Process.GetProcessesByName()</c>.
/// </summary>
public static Process? GetProcess(string processName)
{
return Process.GetProcessesByName(processName).FirstOrDefault();
}

/// <summary>
/// Starts a new process.
/// </summary>
public static void StartProcess(string fileName, string? workingDirectory = null)
{
var process = new Process();
process.StartInfo.FileName = fileName;
process.StartInfo.WorkingDirectory = workingDirectory ?? Directory.GetCurrentDirectory();
process.StartInfo.UseShellExecute = true;
process.Start();
}
}
6 changes: 6 additions & 0 deletions StreamDeckSimHub.sln
Original file line number Diff line number Diff line change
@@ -14,6 +14,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFiles", "SolutionFi
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StreamDeckSimHub.PluginTests", "StreamDeckSimHub.PluginTests\StreamDeckSimHub.PluginTests.csproj", "{D36BDE59-5F66-4311-9105-ABE072AFED82}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StreamDeckSimHub.Installer", "StreamDeckSimHub.Installer\StreamDeckSimHub.Installer.csproj", "{1F5DC759-9721-48BE-AD84-CE582C405C59}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -28,6 +30,10 @@ Global
{D36BDE59-5F66-4311-9105-ABE072AFED82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D36BDE59-5F66-4311-9105-ABE072AFED82}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D36BDE59-5F66-4311-9105-ABE072AFED82}.Release|Any CPU.Build.0 = Release|Any CPU
{1F5DC759-9721-48BE-AD84-CE582C405C59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1F5DC759-9721-48BE-AD84-CE582C405C59}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1F5DC759-9721-48BE-AD84-CE582C405C59}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1F5DC759-9721-48BE-AD84-CE582C405C59}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

0 comments on commit ea469e8

Please sign in to comment.