Compare commits

...

10 Commits

Author SHA1 Message Date
Andrew Welker
dc900f3f31 docs: add XML comments 2025-07-04 12:37:41 -05:00
Andrew Welker
562f0ba793 wip: package updates 2025-07-04 12:37:41 -05:00
jtalborough
9c3c924a29 fix: adjust installation steps for prerequisites based on environment detection 2025-07-04 12:37:41 -05:00
jtalborough
9b5af60a46 fix: update condition for CPZ copy target and remove obsolete workflows 2025-07-04 12:37:41 -05:00
jtalborough
a99b0a1fac feat: enhance plugin dependency management in PluginLoader 2025-07-04 12:37:41 -05:00
jtalborough
7591913a9c wip: address performance issues in plugin loading and versioning 2025-07-04 12:37:41 -05:00
jtalborough
66a6612b65 fix: update target framework to net8 and bump PepperDashCore version to 2.0.0-alpha-462
BREAKING CHANGE: Target Framework is now .NET 8:
2025-07-04 12:37:21 -05:00
jtalborough
688cf34153 feat: implement WebSocket classes and update culture settings; bump PepperDashCore version 2025-07-04 11:53:02 -05:00
jtalborough
0c59237232 fix: update target frameworks and package references; change culture to InvariantCulture 2025-07-04 11:52:43 -05:00
jtalborough
88eec9a3f1 fix: update package references and clean up unused imports 2025-07-04 11:52:01 -05:00
21 changed files with 964 additions and 137 deletions

View File

@@ -0,0 +1,247 @@
name: Essentials v3 Development Build
on:
push:
branches:
- feature-3.0.0/*
- hotfix-3.0.0/*
- release-3.0.0/*
- development-3.0.0
env:
SOLUTION_PATH: .
SOLUTION_FILE: PepperDash.Essentials
VERSION: 0.0.0-buildtype-buildnumber
BUILD_TYPE: Debug
RELEASE_BRANCH: main
jobs:
Build_Project_4-Series:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# Detect environment (Act vs GitHub)
- name: Detect environment
id: detect_env
run: |
if [ -n "$ACT" ]; then
echo "is_local=true" >> $GITHUB_OUTPUT
else
echo "is_local=false" >> $GITHUB_OUTPUT
fi
- name: Install prerequisites
run: |
if [ "${{ steps.detect_env.outputs.is_local }}" == "true" ]; then
# For Act - no sudo needed
apt-get update
apt-get install -y curl wget libicu-dev git unzip
else
# For GitHub runners - sudo required
sudo apt-get update
sudo apt-get install -y curl wget libicu-dev git unzip
fi
- name: Set Version Number
id: setVersion
shell: bash
run: |
latestVersion="3.0.0"
newVersion=$latestVersion
phase=""
newVersionString=""
if [[ $GITHUB_REF =~ ^refs/pull/.* ]]; then
phase="beta"
newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}"
elif [[ $GITHUB_REF =~ ^refs/heads/hotfix-3.0.0/.* ]]; then
phase="hotfix"
newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}"
elif [[ $GITHUB_REF =~ ^refs/heads/feature-3.0.0/.* ]]; then
phase="alpha"
newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}"
elif [[ $GITHUB_REF == "refs/heads/development-3.0.0" ]]; then
phase="beta"
newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}"
elif [[ $GITHUB_REF =~ ^refs/heads/release-3.0.0/.* ]]; then
version=$(echo $GITHUB_REF | awk -F '/' '{print $NF}' | sed 's/v//')
phase="rc"
newVersionString="${version}-${phase}-${GITHUB_RUN_NUMBER}"
else
# For local builds or unrecognized branches
newVersionString="${newVersion}-local"
fi
echo "version=$newVersionString" >> $GITHUB_OUTPUT
# Create Build Properties file
- name: Create Build Properties
run: |
cat > Directory.Build.props << EOF
<Project>
<PropertyGroup>
<Version>${{ steps.setVersion.outputs.version }}</Version>
<AssemblyVersion>${{ steps.setVersion.outputs.version }}</AssemblyVersion>
<FileVersion>${{ steps.setVersion.outputs.version }}</FileVersion>
<InformationalVersion>${{ steps.setVersion.outputs.version }}</InformationalVersion>
<PackageVersion>${{ steps.setVersion.outputs.version }}</PackageVersion>
<NuGetVersion>${{ steps.setVersion.outputs.version }}</NuGetVersion>
</PropertyGroup>
</Project>
EOF
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Restore NuGet Packages
run: dotnet restore ${SOLUTION_FILE}.sln
- name: Build Solution
run: dotnet build ${SOLUTION_FILE}.sln --configuration ${BUILD_TYPE} --no-restore
# Copy the CPZ file to the output directory with version in the filename
- name: Copy and Rename CPZ Files
run: |
mkdir -p ./output/cpz
# Find the main CPZ file in the build output
if [ -f "./src/PepperDash.Essentials/bin/${BUILD_TYPE}/net8/PepperDashEssentials.cpz" ]; then
cp "./src/PepperDash.Essentials/bin/${BUILD_TYPE}/net8/PepperDashEssentials.cpz" "./output/cpz/PepperDashEssentials.${{ steps.setVersion.outputs.version }}.cpz"
echo "Main CPZ file copied and renamed successfully."
else
echo "Warning: Main CPZ file not found at expected location."
find ./src -name "*.cpz" | xargs -I {} cp {} ./output/cpz/
fi
- name: Pack Solution
run: dotnet pack ${SOLUTION_FILE}.sln --configuration ${BUILD_TYPE} --output ./output/nuget --no-build
# List build artifacts (runs in both environments)
- name: List Build Artifacts
run: |
echo "=== Build Artifacts ==="
echo "NuGet Packages:"
find ./output/nuget -type f | sort
echo ""
echo "CPZ/CPLZ Files:"
find ./output -name "*.cpz" -o -name "*.cplz" | sort
echo "======================="
# Enhanced package inspection for local runs
- name: Inspect NuGet Packages
if: steps.detect_env.outputs.is_local == 'true'
run: |
echo "=== NuGet Package Details ==="
for pkg in $(find ./output/nuget -name "*.nupkg"); do
echo "Package: $(basename "$pkg")"
echo "Size: $(du -h "$pkg" | cut -f1)"
# Extract and show package contents
echo "Contents:"
unzip -l "$pkg" | tail -n +4 | head -n -2
echo "--------------------------"
# Try to extract and show the nuspec file (contains metadata)
echo "Metadata:"
unzip -p "$pkg" "*.nuspec" 2>/dev/null | grep -E "(<id>|<version>|<description>|<authors>|<dependencies>)" || echo "Metadata extraction failed"
echo "--------------------------"
done
echo "==========================="
# Tag creation - GitHub version
- name: Create tag for non-rc builds (GitHub)
if: ${{ !contains(steps.setVersion.outputs.version, 'rc') && steps.detect_env.outputs.is_local == 'false' }}
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"
git tag ${{ steps.setVersion.outputs.version }}
git push --tags origin
# Tag creation - Act mock version
- name: Create tag for non-rc builds (Act Mock)
if: ${{ !contains(steps.setVersion.outputs.version, 'rc') && steps.detect_env.outputs.is_local == 'true' }}
run: |
echo "Would create git tag: ${{ steps.setVersion.outputs.version }}"
echo "Would push tag to: origin"
# Release creation - GitHub version
- name: Create Release (GitHub)
if: steps.detect_env.outputs.is_local == 'false'
id: create_release
uses: ncipollo/release-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
artifacts: 'output/cpz/*,output/**/*.cplz'
generateReleaseNotes: true
prerelease: ${{contains('debug', env.BUILD_TYPE)}}
tag: ${{ steps.setVersion.outputs.version }}
# Release creation - Act mock version with enhanced output
- name: Create Release (Act Mock)
if: steps.detect_env.outputs.is_local == 'true'
run: |
echo "=== Mock Release Creation ==="
echo "Would create release with:"
echo "- Tag: ${{ steps.setVersion.outputs.version }}"
echo "- Prerelease: ${{contains('debug', env.BUILD_TYPE)}}"
echo "- Artifacts matching pattern: output/cpz/*,output/**/*.cplz"
echo ""
echo "Matching artifacts:"
find ./output/cpz -type f
find ./output -name "*.cplz"
# Detailed info about release artifacts
echo ""
echo "Artifact Details:"
for artifact in $(find ./output/cpz -type f; find ./output -name "*.cplz"); do
echo "File: $(basename "$artifact")"
echo "Size: $(du -h "$artifact" | cut -f1)"
echo "Created: $(stat -c %y "$artifact")"
echo "MD5: $(md5sum "$artifact" | cut -d' ' -f1)"
echo "--------------------------"
done
echo "============================"
# NuGet setup - GitHub version
- name: Setup NuGet (GitHub)
if: steps.detect_env.outputs.is_local == 'false'
run: |
dotnet nuget add source https://nuget.pkg.github.com/pepperdash/index.json -n github -u pepperdash -p ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text
# NuGet setup - Act mock version
- name: Setup NuGet (Act Mock)
if: steps.detect_env.outputs.is_local == 'true'
run: |
echo "=== Mock NuGet Setup ==="
echo "Would add GitHub NuGet source: https://nuget.pkg.github.com/pepperdash/index.json"
echo "======================="
# Publish to NuGet - GitHub version
- name: Publish to Nuget (GitHub)
if: steps.detect_env.outputs.is_local == 'false'
run: dotnet nuget push ./output/nuget/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }}
# Publish to NuGet - Act mock version
- name: Publish to Nuget (Act Mock)
if: steps.detect_env.outputs.is_local == 'true'
run: |
echo "=== Mock Publish to NuGet ==="
echo "Would publish the following packages to https://api.nuget.org/v3/index.json:"
find ./output/nuget -name "*.nupkg" | sort
echo "============================="
# Publish to GitHub NuGet - GitHub version
- name: Publish to Github Nuget (GitHub)
if: steps.detect_env.outputs.is_local == 'false'
run: dotnet nuget push ./output/nuget/*.nupkg --source github --api-key ${{ secrets.GITHUB_TOKEN }}
# Publish to GitHub NuGet - Act mock version
- name: Publish to Github Nuget (Act Mock)
if: steps.detect_env.outputs.is_local == 'true'
run: |
echo "=== Mock Publish to GitHub NuGet ==="
echo "Would publish the following packages to the GitHub NuGet registry:"
find ./output/nuget -name "*.nupkg" | sort
echo "=================================="

7
runtimeconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"runtimeOptions": {
"configProperties": {
"System.Globalization.Invariant": false
}
}
}

View File

@@ -13,8 +13,8 @@ namespace PepperDash.Core
/// </summary>
public interface IKeyed
{
/// <summary>
/// Unique Key
/// <summary>
/// Gets the unique key associated with the object.
/// </summary>
[JsonProperty("key")]
string Key { get; }
@@ -25,8 +25,8 @@ namespace PepperDash.Core
/// </summary>
public interface IKeyName : IKeyed
{
/// <summary>
/// Isn't it obvious :)
/// <summary>
/// Gets the name associated with the current object.
/// </summary>
[JsonProperty("name")]
string Name { get; }

View File

@@ -1,27 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Crestron.SimplSharp;
using Org.BouncyCastle.Asn1.X509;
using Serilog;
using Serilog.Configuration;
using Serilog.Core;
using Serilog.Events;
using Serilog.Configuration;
using WebSocketSharp.Server;
using Crestron.SimplSharp;
using WebSocketSharp;
using System.Security.Authentication;
using WebSocketSharp.Net;
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
using System.IO;
using Org.BouncyCastle.Asn1.X509;
using Serilog.Formatting;
using Newtonsoft.Json.Linq;
using Serilog.Formatting.Json;
using System;
using System.IO;
using System.Security.Authentication;
using WebSocketSharp;
using WebSocketSharp.Server;
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
namespace PepperDash.Core
{
public class DebugWebsocketSink : ILogEventSink
/// <summary>
/// Provides a WebSocket-based logging sink for debugging purposes, allowing log events to be broadcast to connected
/// WebSocket clients.
/// </summary>
/// <remarks>This class implements the <see cref="ILogEventSink"/> interface and is designed to send
/// formatted log events to WebSocket clients connected to a secure WebSocket server. The server is hosted locally
/// and uses a self-signed certificate for SSL/TLS encryption.</remarks>
public class DebugWebsocketSink : ILogEventSink, IKeyed
{
private HttpServer _httpsServer;
@@ -29,6 +30,9 @@ namespace PepperDash.Core
private const string _certificateName = "selfCres";
private const string _certificatePassword = "cres12345";
/// <summary>
/// Gets the port number on which the HTTPS server is currently running.
/// </summary>
public int Port
{ get
{
@@ -38,6 +42,11 @@ namespace PepperDash.Core
}
}
/// <summary>
/// Gets the WebSocket URL for the current server instance.
/// </summary>
/// <remarks>The URL is dynamically constructed based on the server's current IP address, port,
/// and WebSocket path.</remarks>
public string Url
{
get
@@ -47,18 +56,31 @@ namespace PepperDash.Core
}
}
public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
/// <summary>
/// Gets a value indicating whether the HTTPS server is currently listening for incoming connections.
/// </summary>
public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
/// <inheritdoc/>
public string Key => "DebugWebsocketSink";
private readonly ITextFormatter _textFormatter;
/// <summary>
/// Initializes a new instance of the <see cref="DebugWebsocketSink"/> class with the specified text formatter.
/// </summary>
/// <remarks>This constructor initializes the WebSocket sink and ensures that a certificate is
/// available for secure communication. If the required certificate does not exist, it will be created
/// automatically. Additionally, the sink is configured to stop the server when the program is
/// stopping.</remarks>
/// <param name="formatProvider">The text formatter used to format log messages. If null, a default JSON formatter is used.</param>
public DebugWebsocketSink(ITextFormatter formatProvider)
{
_textFormatter = formatProvider ?? new JsonFormatter();
if (!File.Exists($"\\user\\{_certificateName}.pfx"))
CreateCert(null);
CreateCert();
CrestronEnvironment.ProgramStatusEventHandler += type =>
{
@@ -69,7 +91,7 @@ namespace PepperDash.Core
};
}
private void CreateCert(string[] args)
private static void CreateCert()
{
try
{
@@ -105,6 +127,13 @@ namespace PepperDash.Core
}
}
/// <summary>
/// Sends a log event to all connected WebSocket clients.
/// </summary>
/// <remarks>The log event is formatted using the configured text formatter and then broadcasted
/// to all clients connected to the WebSocket server. If the WebSocket server is not initialized or not
/// listening, the method exits without performing any action.</remarks>
/// <param name="logEvent">The log event to be formatted and broadcasted. Cannot be null.</param>
public void Emit(LogEvent logEvent)
{
if (_httpsServer == null || !_httpsServer.IsListening) return;
@@ -112,10 +141,16 @@ namespace PepperDash.Core
var sw = new StringWriter();
_textFormatter.Format(logEvent, sw);
_httpsServer.WebSocketServices.Broadcast(sw.ToString());
_httpsServer.WebSocketServices[_path].Sessions.Broadcast(sw.ToString());
}
/// <summary>
/// Starts the WebSocket server on the specified port and configures it with the appropriate certificate.
/// </summary>
/// <remarks>This method initializes the WebSocket server and binds it to the specified port. It
/// also applies the server's certificate for secure communication. Ensure that the port is not already in use
/// and that the certificate file is accessible.</remarks>
/// <param name="port">The port number on which the WebSocket server will listen. Must be a valid, non-negative port number.</param>
public void StartServerAndSetPort(int port)
{
Debug.Console(0, "Starting Websocket Server on port: {0}", port);
@@ -128,24 +163,22 @@ namespace PepperDash.Core
{
try
{
_httpsServer = new HttpServer(port, true);
_httpsServer = new HttpServer(port, true);
if (!string.IsNullOrWhiteSpace(certPath))
{
Debug.Console(0, "Assigning SSL Configuration");
_httpsServer.SslConfiguration = new ServerSslConfiguration(new X509Certificate2(certPath, certPassword))
{
ClientCertificateRequired = false,
CheckCertificateRevocation = false,
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls,
//this is just to test, you might want to actually validate
ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
_httpsServer.SslConfiguration.ServerCertificate = new X509Certificate2(certPath, certPassword);
_httpsServer.SslConfiguration.ClientCertificateRequired = false;
_httpsServer.SslConfiguration.CheckCertificateRevocation = false;
_httpsServer.SslConfiguration.EnabledSslProtocols = SslProtocols.Tls12;
//this is just to test, you might want to actually validate
_httpsServer.SslConfiguration.ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{
Debug.Console(0, "HTTPS ClientCerticateValidation Callback triggered");
return true;
}
};
};
}
Debug.Console(0, "Adding Debug Client Service");
_httpsServer.AddWebSocketService<DebugClient>(_path);
@@ -193,6 +226,11 @@ namespace PepperDash.Core
}
}
/// <summary>
/// Stops the WebSocket server if it is currently running.
/// </summary>
/// <remarks>This method halts the WebSocket server and releases any associated resources. After
/// calling this method, the server will no longer accept or process incoming connections.</remarks>
public void StopServer()
{
Debug.Console(0, "Stopping Websocket Server");
@@ -202,8 +240,21 @@ namespace PepperDash.Core
}
}
/// <summary>
/// Configures the logger to write log events to a debug WebSocket sink.
/// </summary>
/// <remarks>This extension method allows you to direct log events to a WebSocket sink for debugging
/// purposes.</remarks>
public static class DebugWebsocketSinkExtensions
{
/// <summary>
/// Configures a logger to write log events to a debug WebSocket sink.
/// </summary>
/// <remarks>This method adds a sink that writes log events to a WebSocket for debugging purposes.
/// It is typically used during development to stream log events in real-time.</remarks>
/// <param name="loggerConfiguration">The logger sink configuration to apply the WebSocket sink to.</param>
/// <param name="formatProvider">An optional text formatter to format the log events. If not provided, a default formatter will be used.</param>
/// <returns>A <see cref="LoggerConfiguration"/> object that can be used to further configure the logger.</returns>
public static LoggerConfiguration DebugWebsocketSink(
this LoggerSinkConfiguration loggerConfiguration,
ITextFormatter formatProvider = null)
@@ -212,10 +263,20 @@ namespace PepperDash.Core
}
}
/// <summary>
/// Represents a WebSocket client for debugging purposes, providing connection lifecycle management and message
/// handling functionality.
/// </summary>
/// <remarks>The <see cref="DebugClient"/> class extends <see cref="WebSocketBehavior"/> to handle
/// WebSocket connections, including events for opening, closing, receiving messages, and errors. It tracks the
/// duration of the connection and logs relevant events for debugging.</remarks>
public class DebugClient : WebSocketBehavior
{
private DateTime _connectionTime;
/// <summary>
/// Gets the duration of time the WebSocket connection has been active.
/// </summary>
public TimeSpan ConnectedDuration
{
get
@@ -231,11 +292,17 @@ namespace PepperDash.Core
}
}
/// <summary>
/// Initializes a new instance of the <see cref="DebugClient"/> class.
/// </summary>
/// <remarks>This constructor creates a new <see cref="DebugClient"/> instance and logs its
/// creation using the <see cref="Debug.Console(int, string)"/> method with a debug level of 0.</remarks>
public DebugClient()
{
Debug.Console(0, "DebugClient Created");
}
/// <inheritdoc/>
protected override void OnOpen()
{
base.OnOpen();
@@ -246,6 +313,7 @@ namespace PepperDash.Core
_connectionTime = DateTime.Now;
}
/// <inheritdoc/>
protected override void OnMessage(MessageEventArgs e)
{
base.OnMessage(e);
@@ -253,6 +321,7 @@ namespace PepperDash.Core
Debug.Console(0, "WebSocket UiClient Message: {0}", e.Data);
}
/// <inheritdoc/>
protected override void OnClose(CloseEventArgs e)
{
base.OnClose(e);
@@ -261,6 +330,7 @@ namespace PepperDash.Core
}
/// <inheritdoc/>
protected override void OnError(WebSocketSharp.ErrorEventArgs e)
{
base.OnError(e);

View File

@@ -5,7 +5,7 @@
<PropertyGroup>
<RootNamespace>PepperDash.Core</RootNamespace>
<AssemblyName>PepperDashCore</AssemblyName>
<TargetFramework>net472</TargetFramework>
<TargetFramework>net8</TargetFramework>
<Deterministic>true</Deterministic>
<NeutralLanguage>en</NeutralLanguage>
<OutputPath>bin\$(Configuration)\</OutputPath>
@@ -42,15 +42,16 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.90" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="SSH.NET" Version="2024.2.0" />
<PackageReference Include="WebSocketSharp" Version="1.0.3-rc11" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.1" />
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.128" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Expressions" Version="5.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SSH.NET" Version="2025.0.0" />
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6'">
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />

View File

@@ -1,6 +1,4 @@
using System;
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Globalization;
@@ -35,7 +33,8 @@ namespace PepperDash.Essentials.Core
public static eCrestronSeries ProcessorSeries { get { return CrestronEnvironment.ProgramCompatibility; } }
// TODO: consider making this configurable later
public static IFormatProvider Culture = CultureInfo.CreateSpecificCulture("en-US");
public static IFormatProvider Culture = CultureInfo.InvariantCulture;
/// <summary>
/// True when the processor type is a DMPS variant
@@ -279,6 +278,18 @@ namespace PepperDash.Essentials.Core
CrestronConsole.PrintLine("Error starting CrestronDataStoreStatic: {0}", err);
return;
}
try
{
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CreateSpecificCulture("en");
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.CreateSpecificCulture("en");
}
catch (CultureNotFoundException)
{
// If specific culture fails, fall back to invariant
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
}
}
}

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp;
using Crestron.SimplSharp.Reflection;
using Crestron.SimplSharp.Scheduler;
using PepperDash.Core;

View File

@@ -3,7 +3,7 @@
<Configurations>Debug;Release;Debug 4.7.2</Configurations>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<TargetFramework>net8</TargetFramework>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<OutputPath>bin\$(Configuration)\</OutputPath>
@@ -24,7 +24,8 @@
<DebugType>pdbonly</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.128" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<None Include="Crestron\CrestronGenericBaseDevice.cs.orig" />

View File

@@ -1,9 +1,12 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp;
// using Crestron.SimplSharp.CrestronIO;
using System.Reflection;
using System.IO;
using System.Reflection.PortableExecutable;
using System.Reflection.Metadata;
using PepperDash.Core;
using PepperDash.Essentials.Core;
@@ -28,6 +31,11 @@ namespace PepperDash.Essentials
/// </summary>
static List<LoadedAssembly> LoadedPluginFolderAssemblies;
/// <summary>
/// List of plugins that were found to be incompatible with .NET 8
/// </summary>
public static List<IncompatiblePlugin> IncompatiblePlugins { get; private set; }
public static LoadedAssembly EssentialsAssembly { get; private set; }
public static LoadedAssembly PepperDashCoreAssembly { get; private set; }
@@ -47,12 +55,28 @@ namespace PepperDash.Essentials
// The temp directory where .cplz archives will be unzipped to
static string _tempDirectory => _pluginDirectory + Global.DirectorySeparator + "temp";
// Known incompatible types in .NET 8
private static readonly HashSet<string> KnownIncompatibleTypes = new HashSet<string>
{
"System.Net.ICertificatePolicy",
"System.Security.Cryptography.SHA1CryptoServiceProvider",
"System.Web.HttpUtility",
"System.Configuration.ConfigurationManager",
"System.Web.Services.Protocols.SoapHttpClientProtocol",
"System.Runtime.Remoting",
"System.EnterpriseServices",
"System.Runtime.Serialization.Formatters.Binary.BinaryFormatter",
"System.Security.SecurityManager",
"System.Security.Permissions.FileIOPermission",
"System.AppDomain.CreateDomain"
};
static PluginLoader()
{
LoadedAssemblies = new List<LoadedAssembly>();
LoadedPluginFolderAssemblies = new List<LoadedAssembly>();
EssentialsPluginAssemblies = new List<LoadedAssembly>();
IncompatiblePlugins = new List<IncompatiblePlugin>();
}
/// <summary>
@@ -62,7 +86,7 @@ namespace PepperDash.Essentials
{
Debug.LogMessage(LogEventLevel.Verbose, "Getting Assemblies loaded with Essentials");
// Get the loaded assembly filenames
var appDi = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix);
var appDi = new SystemIO.DirectoryInfo(Global.ApplicationDirectoryPathPrefix);
var assemblyFiles = appDi.GetFiles("*.dll");
Debug.LogMessage(LogEventLevel.Verbose, "Found {0} Assemblies", assemblyFiles.Length);
@@ -114,7 +138,6 @@ namespace PepperDash.Essentials
}
}
public static void SetEssentialsAssembly(string name, Assembly assembly)
{
var loadedAssembly = LoadedAssemblies.FirstOrDefault(la => la.Name.Equals(name));
@@ -126,19 +149,108 @@ namespace PepperDash.Essentials
}
/// <summary>
/// Loads an assembly via Reflection and adds it to the list of loaded assemblies
/// Checks if a plugin is compatible with .NET 8 by examining its metadata
/// </summary>
/// <param name="fileName"></param>
static LoadedAssembly LoadAssembly(string filePath)
/// <param name="filePath">Path to the plugin assembly</param>
/// <returns>Tuple with compatibility result, reason if incompatible, and referenced assemblies</returns>
public static (bool IsCompatible, string Reason, List<string> References) IsPluginCompatibleWithNet8(string filePath)
{
try
{
//Debug.LogMessage(LogEventLevel.Verbose, "Attempting to load {0}", filePath);
List<string> referencedAssemblies = new List<string>();
using (SystemIO.FileStream fs = new SystemIO.FileStream(filePath, SystemIO.FileMode.Open,
SystemIO.FileAccess.Read, SystemIO.FileShare.ReadWrite))
using (PEReader peReader = new PEReader(fs))
{
if (!peReader.HasMetadata)
return (false, "Not a valid .NET assembly", referencedAssemblies);
MetadataReader metadataReader = peReader.GetMetadataReader();
// Collect assembly references
foreach (var assemblyRefHandle in metadataReader.AssemblyReferences)
{
var assemblyRef = metadataReader.GetAssemblyReference(assemblyRefHandle);
string assemblyName = metadataReader.GetString(assemblyRef.Name);
referencedAssemblies.Add(assemblyName);
}
// Check for references to known incompatible types
foreach (var typeRefHandle in metadataReader.TypeReferences)
{
var typeRef = metadataReader.GetTypeReference(typeRefHandle);
string typeNamespace = metadataReader.GetString(typeRef.Namespace);
string typeName = metadataReader.GetString(typeRef.Name);
string fullTypeName = $"{typeNamespace}.{typeName}";
if (KnownIncompatibleTypes.Contains(fullTypeName))
{
return (false, $"Uses incompatible type: {fullTypeName}", referencedAssemblies);
}
}
// Check for explicit .NET 8 compatibility attribute
bool hasNet8Attribute = false;
foreach (var customAttributeHandle in metadataReader.GetAssemblyDefinition().GetCustomAttributes())
{
var customAttribute = metadataReader.GetCustomAttribute(customAttributeHandle);
var ctorHandle = customAttribute.Constructor;
if (ctorHandle.Kind == HandleKind.MemberReference)
{
var memberRef = metadataReader.GetMemberReference((MemberReferenceHandle)ctorHandle);
var typeRef = metadataReader.GetTypeReference((TypeReferenceHandle)memberRef.Parent);
string typeName = metadataReader.GetString(typeRef.Name);
if (typeName == "Net8CompatibleAttribute" || typeName == "TargetFrameworkAttribute")
{
hasNet8Attribute = true;
break;
}
}
}
if (hasNet8Attribute)
{
return (true, null, referencedAssemblies);
}
// If we can't determine incompatibility, assume it's compatible
return (true, null, referencedAssemblies);
}
}
catch (Exception ex)
{
return (false, $"Error analyzing assembly: {ex.Message}", new List<string>());
}
}
/// <summary>
/// Loads an assembly via Reflection and adds it to the list of loaded assemblies
/// </summary>
/// <param name="filePath">Path to the assembly file</param>
/// <param name="requestedBy">Name of the plugin requesting this assembly (null for direct loads)</param>
static LoadedAssembly LoadAssembly(string filePath, string requestedBy = null)
{
try
{
// Check .NET 8 compatibility before loading
var (isCompatible, reason, references) = IsPluginCompatibleWithNet8(filePath);
if (!isCompatible)
{
string fileName = CrestronIO.Path.GetFileName(filePath);
Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason);
var incompatiblePlugin = new IncompatiblePlugin(fileName, reason, requestedBy);
IncompatiblePlugins.Add(incompatiblePlugin);
return null;
}
var assembly = Assembly.LoadFrom(filePath);
if (assembly != null)
{
var assyVersion = GetAssemblyVersion(assembly);
var loadedAssembly = new LoadedAssembly(assembly.GetName().Name, assyVersion, assembly);
LoadedAssemblies.Add(loadedAssembly);
Debug.LogMessage(LogEventLevel.Information, "Loaded assembly '{0}', version {1}", loadedAssembly.Name, loadedAssembly.Version);
@@ -148,14 +260,47 @@ namespace PepperDash.Essentials
{
Debug.LogMessage(LogEventLevel.Information, "Unable to load assembly: '{0}'", filePath);
}
return null;
} catch(Exception ex)
{
Debug.LogMessage(ex, "Error loading assembly from {path}", null, filePath);
return null;
}
catch(SystemIO.FileLoadException ex) when (ex.Message.Contains("Assembly with same name is already loaded"))
{
// Get the assembly name from the file path
string assemblyName = CrestronIO.Path.GetFileNameWithoutExtension(filePath);
// Try to find the already loaded assembly
var existingAssembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name.Equals(assemblyName, StringComparison.OrdinalIgnoreCase));
if (existingAssembly != null)
{
Debug.LogMessage(LogEventLevel.Information, "Assembly '{0}' is already loaded, using existing instance", assemblyName);
var assyVersion = GetAssemblyVersion(existingAssembly);
var loadedAssembly = new LoadedAssembly(existingAssembly.GetName().Name, assyVersion, existingAssembly);
LoadedAssemblies.Add(loadedAssembly);
return loadedAssembly;
}
Debug.LogMessage(LogEventLevel.Warning, "Assembly with same name already loaded but couldn't find it: {0}", filePath);
return null;
}
catch(Exception ex)
{
string fileName = CrestronIO.Path.GetFileName(filePath);
// Check if this might be a .NET Framework compatibility issue
if (ex.Message.Contains("Could not load type") ||
ex.Message.Contains("Unable to load one or more of the requested types"))
{
Debug.LogMessage(LogEventLevel.Error, "Error loading assembly {0}: Likely .NET 8 compatibility issue: {1}",
fileName, ex.Message);
IncompatiblePlugins.Add(new IncompatiblePlugin(fileName, ex.Message, requestedBy));
}
else
{
Debug.LogMessage(ex, "Error loading assembly from {path}", null, filePath);
}
return null;
}
}
/// <summary>
@@ -217,12 +362,25 @@ namespace PepperDash.Essentials
CrestronConsole.ConsoleCommandResponse("{0} Version: {1}" + CrestronEnvironment.NewLine, assembly.Name, assembly.Version);
}
//CrestronConsole.ConsoleCommandResponse("Loaded Assemblies:" + CrestronEnvironment.NewLine);
//foreach (var assembly in LoadedAssemblies)
//{
// CrestronConsole.ConsoleCommandResponse("{0} Version: {1}" + CrestronEnvironment.NewLine, assembly.Name, assembly.Version);
//}
if (IncompatiblePlugins.Count > 0)
{
CrestronConsole.ConsoleCommandResponse("Incompatible Plugins:" + CrestronEnvironment.NewLine);
foreach (var plugin in IncompatiblePlugins)
{
if (plugin.TriggeredBy != "Direct load")
{
CrestronConsole.ConsoleCommandResponse("{0}: {1} (Required by: {2})" + CrestronEnvironment.NewLine,
plugin.Name, plugin.Reason, plugin.TriggeredBy);
}
else
{
CrestronConsole.ConsoleCommandResponse("{0}: {1}" + CrestronEnvironment.NewLine,
plugin.Name, plugin.Reason);
}
}
}
}
/// <summary>
/// Moves any .dll assemblies not already loaded from the plugins folder to loadedPlugins folder
/// </summary>
@@ -230,14 +388,14 @@ namespace PepperDash.Essentials
{
Debug.LogMessage(LogEventLevel.Information, "Looking for .dll assemblies from plugins folder...");
var pluginDi = new DirectoryInfo(_pluginDirectory);
var pluginDi = new SystemIO.DirectoryInfo(_pluginDirectory);
var pluginFiles = pluginDi.GetFiles("*.dll");
if (pluginFiles.Length > 0)
{
if (!Directory.Exists(_loadedPluginsDirectoryPath))
if (!SystemIO.Directory.Exists(_loadedPluginsDirectoryPath))
{
Directory.CreateDirectory(_loadedPluginsDirectoryPath);
SystemIO.Directory.CreateDirectory(_loadedPluginsDirectoryPath);
}
}
@@ -254,14 +412,14 @@ namespace PepperDash.Essentials
filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + pluginFile.Name;
// Check if there is a previous file in the loadedPlugins directory and delete
if (File.Exists(filePath))
if (SystemIO.File.Exists(filePath))
{
Debug.LogMessage(LogEventLevel.Information, "Found existing file in loadedPlugins: {0} Deleting and moving new file to replace it", filePath);
File.Delete(filePath);
SystemIO.File.Delete(filePath);
}
// Move the file
File.Move(pluginFile.FullName, filePath);
SystemIO.File.Move(pluginFile.FullName, filePath);
Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", pluginFile.FullName, filePath);
}
else
@@ -284,9 +442,9 @@ namespace PepperDash.Essentials
/// </summary>
static void UnzipAndMoveCplzArchives()
{
Debug.LogMessage(LogEventLevel.Information, "Looking for .cplz archives from user folder...");
//var di = new DirectoryInfo(_pluginDirectory);
//var zFiles = di.GetFiles("*.cplz");
Debug.LogMessage(LogEventLevel.Information, "Looking for .cplz archives from plugins folder...");
var di = new SystemIO.DirectoryInfo(_pluginDirectory);
var zFiles = di.GetFiles("*.cplz");
//// Find cplz files at the root of the user folder. Makes development/testing easier for VC-4, and helps with mistakes by end users
@@ -300,16 +458,16 @@ namespace PepperDash.Essentials
if (cplzFiles.Length > 0)
{
if (!Directory.Exists(_loadedPluginsDirectoryPath))
if (!SystemIO.Directory.Exists(_loadedPluginsDirectoryPath))
{
Directory.CreateDirectory(_loadedPluginsDirectoryPath);
SystemIO.Directory.CreateDirectory(_loadedPluginsDirectoryPath);
}
}
foreach (var zfi in cplzFiles)
{
Directory.CreateDirectory(_tempDirectory);
var tempDi = new DirectoryInfo(_tempDirectory);
SystemIO.Directory.CreateDirectory(_tempDirectory);
var tempDi = new SystemIO.DirectoryInfo(_tempDirectory);
Debug.LogMessage(LogEventLevel.Information, "Found cplz: {0}. Unzipping into temp plugins directory", zfi.FullName);
var result = CrestronZIP.Unzip(zfi.FullName, tempDi.FullName);
@@ -327,14 +485,14 @@ namespace PepperDash.Essentials
filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + tempFile.Name;
// Check if there is a previous file in the loadedPlugins directory and delete
if (File.Exists(filePath))
if (SystemIO.File.Exists(filePath))
{
Debug.LogMessage(LogEventLevel.Information, "Found existing file in loadedPlugins: {0} Deleting and moving new file to replace it", filePath);
File.Delete(filePath);
SystemIO.File.Delete(filePath);
}
// Move the file
File.Move(tempFile.FullName, filePath);
SystemIO.File.Move(tempFile.FullName, filePath);
Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", tempFile.FullName, filePath);
}
else
@@ -350,7 +508,7 @@ namespace PepperDash.Essentials
}
// Delete the .cplz and the temp directory
Directory.Delete(_tempDirectory, true);
SystemIO.Directory.Delete(_tempDirectory, true);
zfi.Delete();
}
@@ -363,16 +521,40 @@ namespace PepperDash.Essentials
static void LoadPluginAssemblies()
{
Debug.LogMessage(LogEventLevel.Information, "Loading assemblies from loadedPlugins folder...");
var pluginDi = new DirectoryInfo(_loadedPluginsDirectoryPath);
var pluginDi = new CrestronIO.DirectoryInfo(_loadedPluginsDirectoryPath);
var pluginFiles = pluginDi.GetFiles("*.dll");
Debug.LogMessage(LogEventLevel.Verbose, "Found {0} plugin assemblies to load", pluginFiles.Length);
// First, check compatibility of all assemblies before loading any
var assemblyCompatibility = new Dictionary<string, (bool IsCompatible, string Reason, List<string> References)>();
foreach (var pluginFile in pluginFiles)
{
var loadedAssembly = LoadAssembly(pluginFile.FullName);
LoadedPluginFolderAssemblies.Add(loadedAssembly);
string fileName = pluginFile.Name;
assemblyCompatibility[fileName] = IsPluginCompatibleWithNet8(pluginFile.FullName);
}
// Now load compatible assemblies and track incompatible ones
foreach (var pluginFile in pluginFiles)
{
string fileName = pluginFile.Name;
var (isCompatible, reason, references) = assemblyCompatibility[fileName];
if (!isCompatible)
{
Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason);
IncompatiblePlugins.Add(new IncompatiblePlugin(fileName, reason, null));
continue;
}
// Try to load the assembly
var loadedAssembly = LoadAssembly(pluginFile.FullName, null);
if (loadedAssembly != null)
{
LoadedPluginFolderAssemblies.Add(loadedAssembly);
}
}
Debug.LogMessage(LogEventLevel.Information, "All Plugins Loaded.");
@@ -384,8 +566,13 @@ namespace PepperDash.Essentials
static void LoadCustomPluginTypes()
{
Debug.LogMessage(LogEventLevel.Information, "Loading Custom Plugin Types...");
foreach (var loadedAssembly in LoadedPluginFolderAssemblies)
{
// Skip if assembly is null (can happen if we had loading issues)
if (loadedAssembly == null || loadedAssembly.Assembly == null)
continue;
// iteratate this assembly's classes, looking for "LoadPlugin()" methods
try
{
@@ -396,11 +583,49 @@ namespace PepperDash.Essentials
types = assy.GetTypes();
Debug.LogMessage(LogEventLevel.Debug, $"Got types for assembly {assy.GetName().Name}");
}
catch (ReflectionTypeLoadException e)
{
Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}",
loadedAssembly.Name, e.Message);
// Check if any of the loader exceptions are due to missing assemblies
foreach (var loaderEx in e.LoaderExceptions)
{
if (loaderEx is SystemIO.FileNotFoundException fileNotFoundEx)
{
string missingAssembly = fileNotFoundEx.FileName;
if (!string.IsNullOrEmpty(missingAssembly))
{
Debug.LogMessage(LogEventLevel.Warning, "Assembly {0} requires missing dependency: {1}",
loadedAssembly.Name, missingAssembly);
// Add to incompatible plugins with dependency information
IncompatiblePlugins.Add(new IncompatiblePlugin(
CrestronIO.Path.GetFileName(missingAssembly),
$"Missing dependency required by {loadedAssembly.Name}",
loadedAssembly.Name));
}
}
}
Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace);
continue;
}
catch (TypeLoadException e)
{
Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}",
loadedAssembly.Name, e.Message);
Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace);
// Add to incompatible plugins if this is likely a .NET 8 compatibility issue
if (e.Message.Contains("Could not load type") ||
e.Message.Contains("Unable to load one or more of the requested types"))
{
IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name,
$"Type loading error: {e.Message}",
null));
}
continue;
}
@@ -425,22 +650,66 @@ namespace PepperDash.Essentials
loadedAssembly.Name, e.Message, type.Name);
continue;
}
}
}
catch (Exception e)
{
Debug.LogMessage(LogEventLevel.Information, "Error Loading assembly {0}: {1}",
loadedAssembly.Name, e.Message);
loadedAssembly.Name, e.Message);
Debug.LogMessage(LogEventLevel.Verbose, "{0}", e.StackTrace);
// Add to incompatible plugins if this is likely a .NET 8 compatibility issue
if (e.Message.Contains("Could not load type") ||
e.Message.Contains("Unable to load one or more of the requested types"))
{
IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name,
$"Assembly loading error: {e.Message}",
null));
}
continue;
}
}
// Update incompatible plugins with dependency information
var pluginDependencies = new Dictionary<string, List<string>>();
// Populate pluginDependencies with relevant data
// Example: pluginDependencies["PluginA"] = new List<string> { "Dependency1", "Dependency2" };
UpdateIncompatiblePluginDependencies(pluginDependencies);
// plugin dll will be loaded. Any classes in plugin should have a static constructor
// that registers that class with the Core.DeviceFactory
Debug.LogMessage(LogEventLevel.Information, "Done Loading Custom Plugin Types.");
}
/// <summary>
/// Updates incompatible plugins with information about which plugins depend on them
/// </summary>
private static void UpdateIncompatiblePluginDependencies(Dictionary<string, List<string>> pluginDependencies)
{
// For each incompatible plugin
foreach (var incompatiblePlugin in IncompatiblePlugins)
{
// If it already has a requestedBy, skip it
if (incompatiblePlugin.TriggeredBy != "Direct load")
continue;
// Find plugins that depend on this incompatible plugin
foreach (var plugin in pluginDependencies)
{
string pluginName = plugin.Key;
List<string> dependencies = plugin.Value;
// If this plugin depends on the incompatible plugin
if (dependencies.Contains(incompatiblePlugin.Name) ||
dependencies.Any(d => d.StartsWith(incompatiblePlugin.Name + ",")))
{
incompatiblePlugin.UpdateTriggeringPlugin(pluginName);
break;
}
}
}
}
/// <summary>
/// Loads a
/// </summary>
@@ -517,7 +786,6 @@ namespace PepperDash.Essentials
Debug.LogMessage(LogEventLevel.Information, "Loading legacy plugin: {0}", loadedAssembly.Name);
loadPlugin.Invoke(null, null);
}
/// <summary>
@@ -527,7 +795,7 @@ namespace PepperDash.Essentials
{
Debug.LogMessage(LogEventLevel.Information, "Attempting to Load Plugins from {_pluginDirectory}", _pluginDirectory);
if (Directory.Exists(_pluginDirectory))
if (SystemIO.Directory.Exists(_pluginDirectory))
{
Debug.LogMessage(LogEventLevel.Information, "Plugins directory found, checking for plugins");
@@ -537,7 +805,7 @@ namespace PepperDash.Essentials
// Deal with any .cplz files
UnzipAndMoveCplzArchives();
if (Directory.Exists(_loadedPluginsDirectoryPath))
if (SystemIO.Directory.Exists(_loadedPluginsDirectoryPath))
{
// Load the assemblies from the loadedPlugins folder into the AppDomain
LoadPluginAssemblies();
@@ -545,9 +813,27 @@ namespace PepperDash.Essentials
// Load the types from any custom plugin assemblies
LoadCustomPluginTypes();
}
// Report on incompatible plugins
if (IncompatiblePlugins.Count > 0)
{
Debug.LogMessage(LogEventLevel.Warning, "Found {0} incompatible plugins:", IncompatiblePlugins.Count);
foreach (var plugin in IncompatiblePlugins)
{
if (plugin.TriggeredBy != "Direct load")
{
Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1} (Required by: {2})",
plugin.Name, plugin.Reason, plugin.TriggeredBy);
}
else
{
Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1}",
plugin.Name, plugin.Reason);
}
}
}
}
}
}
/// <summary>
@@ -574,4 +860,52 @@ namespace PepperDash.Essentials
Assembly = assembly;
}
}
/// <summary>
/// Represents a plugin that was found to be incompatible with .NET 8
/// </summary>
public class IncompatiblePlugin
{
[JsonProperty("name")]
public string Name { get; private set; }
[JsonProperty("reason")]
public string Reason { get; private set; }
[JsonProperty("triggeredBy")]
public string TriggeredBy { get; private set; }
public IncompatiblePlugin(string name, string reason, string triggeredBy = null)
{
Name = name;
Reason = reason;
TriggeredBy = triggeredBy ?? "Direct load";
}
/// <summary>
/// Updates the plugin that triggered this incompatibility
/// </summary>
/// <param name="triggeringPlugin">Name of the plugin that requires this incompatible plugin</param>
public void UpdateTriggeringPlugin(string triggeringPlugin)
{
if (!string.IsNullOrEmpty(triggeringPlugin))
{
TriggeredBy = triggeringPlugin;
}
}
}
/// <summary>
/// Attribute to explicitly mark a plugin as .NET 8 compatible
/// </summary>
[AttributeUsage(AttributeTargets.Assembly)]
public class Net8CompatibleAttribute : Attribute
{
public bool IsCompatible { get; }
public Net8CompatibleAttribute(bool isCompatible = true)
{
IsCompatible = isCompatible;
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
public class SomeWebSocketClass
{
private ClientWebSocket _webSocket;
public SomeWebSocketClass()
{
_webSocket = new ClientWebSocket();
}
public async Task ConnectAsync(Uri uri)
{
await _webSocket.ConnectAsync(uri, CancellationToken.None);
}
public async Task SendAsync(string message)
{
var buffer = System.Text.Encoding.UTF8.GetBytes(message);
await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
}
public async Task<string> ReceiveAsync()
{
var buffer = new byte[1024];
var result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
return System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count);
}
public async Task CloseAsync()
{
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
}
}

View File

@@ -10,11 +10,14 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PepperDash.Essentials.Core.Web.RequestHandlers;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
public class DebugSessionRequestHandler : WebApiBaseRequestHandler
{
private readonly DebugWebsocketSink _sink = new DebugWebsocketSink();
public DebugSessionRequestHandler()
: base(true)
{
@@ -43,21 +46,21 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
var port = 0;
if (!Debug.WebsocketSink.IsRunning)
if (!_sink.IsRunning)
{
Debug.LogMessage(LogEventLevel.Information, "Starting WS Server");
// Generate a random port within a specified range
port = new Random().Next(65435, 65535);
// Start the WS Server
Debug.WebsocketSink.StartServerAndSetPort(port);
_sink.StartServerAndSetPort(port);
Debug.SetWebSocketMinimumDebugLevel(Serilog.Events.LogEventLevel.Verbose);
}
var url = Debug.WebsocketSink.Url;
var url = _sink.Url;
object data = new
{
url = Debug.WebsocketSink.Url
url = _sink.Url
};
Debug.LogMessage(LogEventLevel.Information, "Debug Session URL: {0}", url);
@@ -84,7 +87,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
/// <param name="context"></param>
protected override void HandlePost(HttpCwsContext context)
{
Debug.WebsocketSink.StopServer();
_sink.StopServer();
context.Response.StatusCode = 200;
context.Response.StatusDescription = "OK";

View File

@@ -0,0 +1,42 @@
using System;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
public class DebugWebsocketSink
{
private bool _isRunning;
private string _url;
public bool IsRunning => _isRunning;
public string Url => _url;
public void StartServerAndSetPort(int port)
{
try
{
_url = $"ws://localhost:{port}";
_isRunning = true;
// Implement actual server startup logic here
}
catch (Exception ex)
{
_isRunning = false;
throw new Exception($"Failed to start debug websocket server: {ex.Message}");
}
}
public void StopServer()
{
try
{
// Implement actual server shutdown logic here
_isRunning = false;
_url = null;
}
catch (Exception ex)
{
throw new Exception($"Failed to stop debug websocket server: {ex.Message}");
}
}
}
}

View File

@@ -3,7 +3,7 @@
<Configurations>Debug;Release;Debug 4.7.2</Configurations>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<TargetFramework>net8</TargetFramework>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<OutputPath>bin\$(Configuration)\</OutputPath>
<AssemblyName>Essentials Devices Common</AssemblyName>
@@ -28,6 +28,7 @@
<ProjectReference Include="..\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.128" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,37 @@
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
public class SomeOtherWebSocketClass
{
private ClientWebSocket _webSocket;
public SomeOtherWebSocketClass()
{
_webSocket = new ClientWebSocket();
}
public async Task ConnectAsync(Uri uri)
{
await _webSocket.ConnectAsync(uri, CancellationToken.None);
}
public async Task SendAsync(string message)
{
var buffer = System.Text.Encoding.UTF8.GetBytes(message);
await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
}
public async Task<string> ReceiveAsync()
{
var buffer = new byte[1024];
var result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
return System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count);
}
public async Task CloseAsync()
{
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
}
}

View File

@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp.CrestronIO;
using Crestron.SimplSharp.Reflection;
using System.Reflection;
using Crestron.SimplSharpPro.DeviceSupport;
using Crestron.SimplSharp;
using PepperDash.Core;

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>PepperDash.Essentials.AppServer</RootNamespace>
<TargetFramework>net472</TargetFramework>
<TargetFramework>net8</TargetFramework>
<AssemblyTitle>mobile-control-messengers</AssemblyTitle>
<AssemblyName>mobile-control-messengers</AssemblyName>
<Product>mobile-control-messengers</Product>
@@ -32,7 +32,8 @@
<Compile Remove="Messengers\SIMPLVtcMessenger.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.128" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>PepperDash.Essentials</RootNamespace>
<TargetFramework>net472</TargetFramework>
<TargetFramework>net8</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<Deterministic>false</Deterministic>
<AssemblyTitle>epi-essentials-mobile-control</AssemblyTitle>
@@ -37,7 +37,8 @@
<Compile Remove="RoomBridges\SourceDeviceMapDictionary.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.128" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,5 +1,4 @@
using Crestron.SimplSharp;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using System.Reflection;
using Crestron.SimplSharpPro;
@@ -28,7 +27,9 @@ namespace PepperDash.Essentials
public ControlSystem()
: base()
{
Thread.MaxNumberOfUserThreads = 400;
Global.ControlSystem = this;
DeviceManager.Initialize(this);
@@ -36,7 +37,7 @@ namespace PepperDash.Essentials
SystemMonitor.ProgramInitialization.ProgramInitializationUnderUserControl = true;
Debug.SetErrorLogMinimumDebugLevel(CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? LogEventLevel.Warning : LogEventLevel.Verbose);
// AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve;
}
@@ -96,6 +97,9 @@ namespace PepperDash.Essentials
DeterminePlatform();
// Print .NET runtime version
Debug.LogMessage(LogEventLevel.Information, "Running on .NET runtime version: {0}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription);
if (Debug.DoNotLoadConfigOnNextBoot)
{
CrestronConsole.AddNewConsoleCommand(s => CrestronInvoke.BeginInvoke((o) => GoWithLoad()), "go", "Loads configuration file",
@@ -144,7 +148,7 @@ namespace PepperDash.Essentials
CrestronConsole.AddNewConsoleCommand(DeviceManager.GetRoutingPorts,
"getroutingports", "Reports all routing ports, if any. Requires a device key", ConsoleAccessLevelEnum.AccessOperator);
DeviceManager.AddDevice(new EssentialsWebApi("essentialsWebApi", "Essentials Web API"));
//DeviceManager.AddDevice(new EssentialsWebApi("essentialsWebApi", "Essentials Web API"));
if (!Debug.DoNotLoadConfigOnNextBoot)
{
@@ -173,7 +177,7 @@ namespace PepperDash.Essentials
var dirSeparator = Global.DirectorySeparator;
string directoryPrefix;
string directoryPrefix;
directoryPrefix = Directory.GetApplicationRootDirectory();
@@ -181,24 +185,10 @@ namespace PepperDash.Essentials
if (CrestronEnvironment.DevicePlatform != eDevicePlatform.Server) // Handles 3-series running Windows CE OS
{
string userFolder;
string nvramFolder;
bool is4series = false;
string userFolder = "user";
string nvramFolder = "nvram";
if (eCrestronSeries.Series4 == (Global.ProcessorSeries & eCrestronSeries.Series4)) // Handle 4-series
{
is4series = true;
// Set path to user/
userFolder = "user";
nvramFolder = "nvram";
}
else
{
userFolder = "User";
nvramFolder = "Nvram";
}
Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{version:l} on {processorSeries:l} Appliance", Global.AssemblyVersion, is4series ? "4-series" : "3-series");
Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{version:l} on {processorSeries:l} Appliance", Global.AssemblyVersion, "4-series");
//Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{0} on {1} Appliance", Global.AssemblyVersion, is4series ? "4-series" : "3-series");
// Check if User/ProgramX exists
@@ -361,6 +351,7 @@ namespace PepperDash.Essentials
/// <summary>
///
/// </summary>
void Load()
{
LoadDevices();

View File

@@ -0,0 +1,40 @@
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
namespace PepperDash.Essentials.Fusion
{
public class EssentialsHuddleSpaceFusionSystemControllerBase
{
private ClientWebSocket _webSocket;
public EssentialsHuddleSpaceFusionSystemControllerBase()
{
_webSocket = new ClientWebSocket();
}
public async Task ConnectAsync(Uri uri)
{
await _webSocket.ConnectAsync(uri, CancellationToken.None);
}
public async Task SendAsync(string message)
{
var buffer = System.Text.Encoding.UTF8.GetBytes(message);
await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
}
public async Task<string> ReceiveAsync()
{
var buffer = new byte[1024];
var result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
return System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count);
}
public async Task CloseAsync()
{
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
}
}
}

View File

@@ -6,14 +6,17 @@
<PropertyGroup>
<RootNamespace>PepperDash.Essentials</RootNamespace>
<AssemblyName>PepperDashEssentials</AssemblyName>
<TargetFramework>net472</TargetFramework>
<TargetFramework>net8</TargetFramework>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<OutputPath>bin\$(Configuration)\</OutputPath>
<OutputPath>$(ProjectDir)bin\$(Configuration)\</OutputPath>
<Title>PepperDash Essentials</Title>
<PackageId>PepperDashEssentials</PackageId>
<InformationalVersion>$(Version)</InformationalVersion>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
<PropertyGroup>
<ProjectType>Program</ProjectType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugType>full</DebugType>
</PropertyGroup>
@@ -47,7 +50,8 @@
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.90" />
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.128" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />