diff --git a/JexusManager.Features.Certificates/CertificatesFeature.cs b/JexusManager.Features.Certificates/CertificatesFeature.cs index 44c3f959..f7c29fb3 100644 --- a/JexusManager.Features.Certificates/CertificatesFeature.cs +++ b/JexusManager.Features.Certificates/CertificatesFeature.cs @@ -10,35 +10,31 @@ * To change this template use Tools | Options | Coding | Edit Standard Headers. */ +using System; using System.ComponentModel; +using System.Security.Cryptography; +using System.Windows.Forms; +using Microsoft.Extensions.Logging; +using JexusManager; +using System.Reflection; +using System.Collections; +using Microsoft.Web.Management.Client.Win32; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Web.Management.Client; +using System.Resources; +using System.Collections.Generic; +using JexusManager.Services; +using Microsoft.Web.Administration; +using System.Diagnostics; +using JexusManager.Features.Certificates.Wizards.CertificateRequestWizard; +using JexusManager.Features.Certificates.Wizards.CertificateRenewWizard; namespace JexusManager.Features.Certificates { - using System; - using System.Collections; - using System.Collections.Generic; - using System.Diagnostics; - using System.Reflection; - using System.Resources; - using System.Security.Cryptography; - using System.Security.Cryptography.X509Certificates; - using System.Windows.Forms; - - using Services; - using Wizards.CertificateRenewWizard; - using Wizards.CertificateRequestWizard; - - using Microsoft.Web.Administration; - using Microsoft.Web.Management.Client; - using Microsoft.Web.Management.Client.Win32; - - using Module = Microsoft.Web.Management.Client.Module; - - /// - /// Description of DefaultDocumentFeature. - /// - internal class CertificatesFeature + public class CertificatesFeature { + private static readonly ILogger _logger = LogHelper.GetLogger("CertificatesFeature"); + private sealed class FeatureTaskList : DefaultTaskList { private readonly CertificatesFeature _owner; @@ -108,11 +104,10 @@ public override ICollection GetTaskItems() } catch (CryptographicException ex) { - if (ex.HResult != NativeMethods.BadKeySet) + if (ex.HResult != Microsoft.Web.Administration.NativeMethods.BadKeySet) { // TODO: add CNG support. - // throw; - Debug.WriteLine($"ate exception in certificates feature {ex}"); + _logger.LogError(ex, "Cryptographic error in certificates feature. HResult: {HResult}", ex.HResult); } } } @@ -218,7 +213,7 @@ public void Trust() } } - public CertificatesFeature(Module module) + public CertificatesFeature(Microsoft.Web.Management.Client.Module module) { Module = module; } @@ -285,9 +280,9 @@ public void Load() } catch (CryptographicException ex) { - if (ex.HResult != NativeMethods.NonExistingStore) + if (ex.HResult != Microsoft.Web.Administration.NativeMethods.NonExistingStore) { - Debug.WriteLine($"CryptographicException {ex.HResult} from CertificatesFeature."); + _logger.LogError(ex, "CryptographicException {HResult} from CertificatesFeature.", ex.HResult); throw; } } @@ -324,6 +319,11 @@ public void Remove() return; } + DeleteCertificate(); + } + + private void DeleteCertificate() + { try { // remove certificate and mapping @@ -348,22 +348,20 @@ public void Remove() } catch (Win32Exception ex) { - // elevation is cancelled. - var message = NativeMethods.KnownCases(ex.NativeErrorCode); + var message = Microsoft.Web.Administration.NativeMethods.KnownCases(ex.NativeErrorCode); if (string.IsNullOrEmpty(message)) - { - Debug.WriteLine(ex); - Debug.WriteLine($"native {ex.NativeErrorCode}"); - // throw; + { + _logger.LogError(ex, "Win32 error deleting certificate. Native error code: {Code}", ex.NativeErrorCode); } else { + var dialog = (IManagementUIService)GetService(typeof(IManagementUIService)); dialog.ShowError(ex, message, Name, false); } } catch (Exception ex) { - Debug.WriteLine(ex); + _logger.LogError(ex, "Error deleting certificate"); } } @@ -457,7 +455,7 @@ private void Trust() } catch (CryptographicException ex) { - if (ex.HResult != NativeMethods.UserCancelled) + if (ex.HResult != Microsoft.Web.Administration.NativeMethods.UserCancelled) { var dialog = (IManagementUIService)GetService(typeof(IManagementUIService)); dialog.ShowMessage($"An unexpected error happened. HResult is {ex.HResult}. Contact your system administrator.", Name, @@ -488,7 +486,7 @@ public virtual Version MinimumFrameworkVersion get { return FxVersionNotRequired; } } - public Module Module { get; } + public Microsoft.Web.Management.Client.Module Module { get; } public string Name { diff --git a/JexusManager.Features.Certificates/CertificatesItem.cs b/JexusManager.Features.Certificates/CertificatesItem.cs index 9688bc10..d638a532 100644 --- a/JexusManager.Features.Certificates/CertificatesItem.cs +++ b/JexusManager.Features.Certificates/CertificatesItem.cs @@ -7,7 +7,7 @@ namespace JexusManager.Features.Certificates using System; using System.Security.Cryptography.X509Certificates; - internal class CertificatesItem : IEquatable + public class CertificatesItem : IEquatable { public CertificatesItem(X509Certificate2 certificate, string store, CertificatesFeature feature) { @@ -25,4 +25,4 @@ public bool Equals(CertificatesItem other) return other != null && other.Certificate.GetCertHashString() == Certificate.GetCertHashString(); } } -} \ No newline at end of file +} diff --git a/JexusManager.Features.Certificates/CompleteRequestDialog.cs b/JexusManager.Features.Certificates/CompleteRequestDialog.cs index 05a19a7d..433e1430 100644 --- a/JexusManager.Features.Certificates/CompleteRequestDialog.cs +++ b/JexusManager.Features.Certificates/CompleteRequestDialog.cs @@ -9,19 +9,23 @@ namespace JexusManager.Features.Certificates { using System; using System.ComponentModel; - using System.Diagnostics; - using System.IO; - using System.Reactive.Disposables; - using System.Reactive.Linq; - using System.Security.Cryptography.X509Certificates; using System.Windows.Forms; + using Microsoft.Extensions.Logging; + using JexusManager; using Microsoft.Web.Management.Client.Win32; using Mono.Security.Authenticode; using static System.Windows.Forms.VisualStyles.VisualStyleElement.Window; + using System.Security.Cryptography.X509Certificates; + using System.Reactive.Disposables; + using System.Reactive.Linq; + using System.IO; + using System.Diagnostics; - internal partial class CompleteRequestDialog : DialogForm + public partial class CompleteRequestDialog : DialogForm { + private static readonly ILogger _logger = LogHelper.GetLogger("CompleteRequestDialog"); + public CompleteRequestDialog(IServiceProvider serviceProvider, CertificatesFeature feature) : base(serviceProvider) { @@ -102,48 +106,7 @@ public CompleteRequestDialog(IServiceProvider serviceProvider, CertificatesFeatu Store = cbStore.SelectedIndex == 0 ? "Personal" : "WebHosting"; - try - { - // add certificate - using var process = new Process(); - var start = process.StartInfo; - start.Verb = "runas"; - start.UseShellExecute = true; - start.FileName = "cmd"; - start.Arguments = $"/c \"\"{CertificateInstallerLocator.FileName}\" /f:\"{p12File}\" /p:{p12pwd} /n:\"{txtName.Text}\" /s:{(cbStore.SelectedIndex == 0 ? "MY" : "WebHosting")}\""; - start.CreateNoWindow = true; - start.WindowStyle = ProcessWindowStyle.Hidden; - process.Start(); - process.WaitForExit(); - File.Delete(p12File); - if (process.ExitCode == 0) - { - DialogResult = DialogResult.OK; - } - else - { - MessageBox.Show(process.ExitCode.ToString()); - } - } - catch (Win32Exception ex) - { - // elevation is cancelled. - var message = Microsoft.Web.Administration.NativeMethods.KnownCases(ex.NativeErrorCode); - if (string.IsNullOrEmpty(message)) - { - Debug.WriteLine(ex); - Debug.WriteLine($"native {ex.NativeErrorCode}"); - // throw; - } - else - { - ShowError(ex, message, false); - } - } - catch (Exception ex) - { - Debug.WriteLine(ex); - } + Install(p12File, p12pwd); })); container.Add( @@ -166,5 +129,44 @@ public CompleteRequestDialog(IServiceProvider serviceProvider, CertificatesFeatu public string Store { get; set; } public X509Certificate2 Item { get; set; } + + private void Install(string p12File, string p12pwd) + { + try + { + // add certificate + using var process = new Process(); + var start = process.StartInfo; + start.Verb = "runas"; + start.UseShellExecute = true; + start.FileName = "cmd"; + start.Arguments = $"/c \"\"{CertificateInstallerLocator.FileName}\" /f:\"{p12File}\" /p:{p12pwd} /n:\"{txtName.Text}\" /s:{(cbStore.SelectedIndex == 0 ? "MY" : "WebHosting")}\""; + start.CreateNoWindow = true; + start.WindowStyle = ProcessWindowStyle.Hidden; + process.Start(); + process.WaitForExit(); + File.Delete(p12File); + if (process.ExitCode == 0) + { + DialogResult = DialogResult.OK; + } + else + { + MessageBox.Show(process.ExitCode.ToString()); + } + } + catch (Win32Exception ex) + { + // elevation is cancelled. + if (ex.NativeErrorCode != (int)Windows.Win32.Foundation.WIN32_ERROR.ERROR_CANCELLED) + { + _logger.LogError(ex, "Win32 error during certificate installation. Native error code: {Code}", ex.NativeErrorCode); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during certificate installation"); + } + } } } diff --git a/JexusManager.Features.Certificates/ImportCertificateDialog.cs b/JexusManager.Features.Certificates/ImportCertificateDialog.cs index 9eda4897..1036ebd1 100644 --- a/JexusManager.Features.Certificates/ImportCertificateDialog.cs +++ b/JexusManager.Features.Certificates/ImportCertificateDialog.cs @@ -8,14 +8,9 @@ namespace JexusManager.Features.Certificates { using System; using System.ComponentModel; - using System.Diagnostics; - using System.IO; - using System.Reactive.Disposables; - using System.Reactive.Linq; - using System.Security.Cryptography; - using System.Security.Cryptography.X509Certificates; - using System.Text; using System.Windows.Forms; + using Microsoft.Extensions.Logging; + using JexusManager; using Services; @@ -26,9 +21,17 @@ namespace JexusManager.Features.Certificates using Org.BouncyCastle.OpenSsl; using Org.BouncyCastle.Security; using static System.Windows.Forms.VisualStyles.VisualStyleElement.Window; + using System.Security.Cryptography.X509Certificates; + using System.Diagnostics; + using System.Reactive.Linq; + using System.Reactive.Disposables; + using System.Text; + using System.IO; internal partial class ImportCertificateDialog : DialogForm { + private static readonly ILogger _logger = LogHelper.GetLogger("ImportCertificateDialog"); + public ImportCertificateDialog(IServiceProvider serviceProvider, CertificatesFeature feature) : base(serviceProvider) { @@ -108,47 +111,7 @@ public ImportCertificateDialog(IServiceProvider serviceProvider, CertificatesFea } else { - try - { - using var process = new Process(); - // add certificate - var start = process.StartInfo; - start.Verb = "runas"; - start.UseShellExecute = true; - start.FileName = "cmd"; - start.Arguments = $"/c \"\"{CertificateInstallerLocator.FileName}\" /f:\"{txtFile.Text}\" /p:{txtPassword.Text} /n:\"{Item.FriendlyName}\" /s:{(cbStore.SelectedIndex == 0 ? "MY" : "WebHosting")}\""; - start.CreateNoWindow = true; - start.WindowStyle = ProcessWindowStyle.Hidden; - process.Start(); - process.WaitForExit(); - if (process.ExitCode == 0) - { - DialogResult = DialogResult.OK; - } - else - { - MessageBox.Show(process.ExitCode.ToString()); - } - } - catch (Win32Exception ex) - { - // elevation is cancelled. - var message = NativeMethods.KnownCases(ex.NativeErrorCode); - if (string.IsNullOrEmpty(message)) - { - Debug.WriteLine(ex); - Debug.WriteLine($"native {ex.NativeErrorCode}"); - // throw; - } - else - { - ShowError(ex, message, false); - } - } - catch (Exception ex) - { - Debug.WriteLine(ex); - } + Install(txtFile.Text, txtPassword.Text); } } catch (Exception ex) @@ -169,5 +132,40 @@ public ImportCertificateDialog(IServiceProvider serviceProvider, CertificatesFea public string Store { get; set; } public X509Certificate2 Item { get; set; } + + private void Install(string fileName, string password) + { + try + { + // install certificate + using var process = new Process(); + var start = process.StartInfo; + start.Verb = "runas"; + start.UseShellExecute = true; + start.FileName = "cmd"; + start.Arguments = $"/c \"\"{CertificateInstallerLocator.FileName}\" /f:\"{fileName}\" /p:\"{password}\" /n:\"{txtName.Text}\" /s:{(cbStore.SelectedIndex == 0 ? "MY" : "WebHosting")}\""; + start.CreateNoWindow = true; + start.WindowStyle = ProcessWindowStyle.Hidden; + process.Start(); + process.WaitForExit(); + + if (process.ExitCode == 0) + { + DialogResult = DialogResult.OK; + } + } + catch (Win32Exception ex) + { + // elevation is cancelled. + if (ex.NativeErrorCode != (int)Windows.Win32.Foundation.WIN32_ERROR.ERROR_CANCELLED) + { + _logger.LogError(ex, "Win32 error during certificate installation. Native error code: {Code}", ex.NativeErrorCode); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during certificate installation"); + } + } } } diff --git a/JexusManager.Features.Certificates/SelfCertificateDialog.cs b/JexusManager.Features.Certificates/SelfCertificateDialog.cs index 684d61d5..d67bac46 100644 --- a/JexusManager.Features.Certificates/SelfCertificateDialog.cs +++ b/JexusManager.Features.Certificates/SelfCertificateDialog.cs @@ -16,7 +16,7 @@ namespace JexusManager.Features.Certificates using System.Reactive.Linq; using System.Security.Cryptography.X509Certificates; using System.Windows.Forms; - + using Microsoft.Extensions.Logging; using Microsoft.Web.Management.Client.Win32; using Org.BouncyCastle.Asn1; using Org.BouncyCastle.Asn1.X509; @@ -33,6 +33,8 @@ namespace JexusManager.Features.Certificates internal partial class SelfCertificateDialog : DialogForm { + private static readonly ILogger _logger = LogHelper.GetLogger("SelfCertificateDialog"); + public SelfCertificateDialog(IServiceProvider serviceProvider, CertificatesFeature feature) : base(serviceProvider) { @@ -183,9 +185,7 @@ public SelfCertificateDialog(IServiceProvider serviceProvider, CertificatesFeatu var message = Microsoft.Web.Administration.NativeMethods.KnownCases(ex.NativeErrorCode); if (string.IsNullOrEmpty(message)) { - Debug.WriteLine(ex); - Debug.WriteLine($"native {ex.NativeErrorCode}"); - // throw; + _logger.LogError(ex, "Win32 error installing certificate. Native error code: {Code}", ex.NativeErrorCode); } else { @@ -195,7 +195,7 @@ public SelfCertificateDialog(IServiceProvider serviceProvider, CertificatesFeatu catch (Exception ex) { ShowError(ex, $"unexpected exception", false); - Debug.WriteLine(ex); + _logger.LogError(ex, "Error installing certificate"); } finally { @@ -209,8 +209,8 @@ public SelfCertificateDialog(IServiceProvider serviceProvider, CertificatesFeatu } catch (Exception ex) { - Debug.WriteLine(ex); ShowError(ex, Text, false); + _logger.LogError(ex, "Error validating certificate"); return; } })); diff --git a/JexusManager.Features.HttpApi/HttpApiFeature.cs b/JexusManager.Features.HttpApi/HttpApiFeature.cs index 131468c5..67528a50 100644 --- a/JexusManager.Features.HttpApi/HttpApiFeature.cs +++ b/JexusManager.Features.HttpApi/HttpApiFeature.cs @@ -7,7 +7,7 @@ namespace JexusManager.Features.HttpApi { - internal abstract class HttpApiFeature : FeatureBase, IHttpApiFeature + public abstract class HttpApiFeature : FeatureBase, IHttpApiFeature where T : class, IItem { protected HttpApiFeature(Module module) diff --git a/JexusManager.Features.HttpApi/IpMappingFeature.cs b/JexusManager.Features.HttpApi/IpMappingFeature.cs index 659c8110..7c32ab00 100644 --- a/JexusManager.Features.HttpApi/IpMappingFeature.cs +++ b/JexusManager.Features.HttpApi/IpMappingFeature.cs @@ -3,26 +3,28 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using JexusManager.Services; -using Microsoft.Web.Administration; -using Microsoft.Web.Management.Client; -using Microsoft.Web.Management.Client.Win32; -using System.Collections; -using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.Reflection; using System.Windows.Forms; +using Microsoft.Extensions.Logging; +using JexusManager; +using System.Reflection; +using System.Collections; +using Microsoft.Web.Management.Client.Win32; +using Microsoft.Web.Management.Client; +using System.Collections.Generic; using Org.BouncyCastle.Utilities.Encoders; +using System.Diagnostics; +using Microsoft.Web.Administration; using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography; -using Exception = System.Exception; +using JexusManager.Services; namespace JexusManager.Features.HttpApi { internal class IpMappingFeature : HttpApiFeature { + private static readonly ILogger _logger = LogHelper.GetLogger("IpMappingFeature"); + private sealed class FeatureTaskList : DefaultTaskList { private readonly IpMappingFeature _owner; @@ -93,6 +95,11 @@ public void Remove() return; } + DeleteMapping(); + } + + private void DeleteMapping() + { try { // remove IP mapping @@ -118,21 +125,14 @@ public void Remove() catch (Win32Exception ex) { // elevation is cancelled. - var message = Microsoft.Web.Administration.NativeMethods.KnownCases(ex.NativeErrorCode); - if (string.IsNullOrEmpty(message)) - { - Debug.WriteLine(ex); - Debug.WriteLine($"native {ex.NativeErrorCode}"); - // throw; - } - else + if (ex.NativeErrorCode != (int)Windows.Win32.Foundation.WIN32_ERROR.ERROR_CANCELLED) { - dialog.ShowError(ex, message, Name, false); + _logger.LogError(ex, "Win32 error deleting IP mapping. Native error code: {Code}", ex.NativeErrorCode); } } catch (Exception ex) { - Debug.WriteLine(ex); + _logger.LogError(ex, "Error deleting IP mapping"); } } diff --git a/JexusManager.Features.HttpApi/ReservedUrlsFeature.cs b/JexusManager.Features.HttpApi/ReservedUrlsFeature.cs index a2acc616..868df014 100644 --- a/JexusManager.Features.HttpApi/ReservedUrlsFeature.cs +++ b/JexusManager.Features.HttpApi/ReservedUrlsFeature.cs @@ -10,16 +10,18 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.Reflection; using System.Windows.Forms; -using Exception = System.Exception; +using Microsoft.Extensions.Logging; +using JexusManager; +using System.Reflection; +using System.Diagnostics; namespace JexusManager.Features.HttpApi { - internal class ReservedUrlsFeature : HttpApiFeature + public class ReservedUrlsFeature : HttpApiFeature { + private static readonly ILogger _logger = LogHelper.GetLogger("ReservedUrlsFeature"); + private sealed class FeatureTaskList : DefaultTaskList { private readonly ReservedUrlsFeature _owner; @@ -94,6 +96,11 @@ public void Remove() return; } + DeleteReservedUrl(); + } + + private void DeleteReservedUrl() + { try { // remove reserved URL @@ -118,20 +125,14 @@ public void Remove() catch (Win32Exception ex) { // elevation is cancelled. - var message = Microsoft.Web.Administration.NativeMethods.KnownCases(ex.NativeErrorCode); - if (string.IsNullOrEmpty(message)) - { - Debug.WriteLine(ex); - Debug.WriteLine($"native {ex.NativeErrorCode}"); - } - else + if (ex.NativeErrorCode != (int)Windows.Win32.Foundation.WIN32_ERROR.ERROR_CANCELLED) { - dialog.ShowError(ex, message, Name, false); + _logger.LogError(ex, "Win32 error deleting reserved URL. Native error code: {Code}", ex.NativeErrorCode); } } catch (Exception ex) { - Debug.WriteLine(ex); + _logger.LogError(ex, "Error deleting reserved URL"); } } diff --git a/JexusManager.Features.HttpApi/ReservedUrlsItem.cs b/JexusManager.Features.HttpApi/ReservedUrlsItem.cs index 438f4909..1be8981c 100644 --- a/JexusManager.Features.HttpApi/ReservedUrlsItem.cs +++ b/JexusManager.Features.HttpApi/ReservedUrlsItem.cs @@ -6,7 +6,7 @@ namespace JexusManager.Features.HttpApi { - internal class ReservedUrlsItem : IItem + public class ReservedUrlsItem : IItem { public string UrlPrefix { get; set; } public string SecurityDescriptor { get; set; } diff --git a/JexusManager.Features.HttpApi/SniMappingFeature.cs b/JexusManager.Features.HttpApi/SniMappingFeature.cs index 2ad54f83..774f331f 100644 --- a/JexusManager.Features.HttpApi/SniMappingFeature.cs +++ b/JexusManager.Features.HttpApi/SniMappingFeature.cs @@ -10,8 +10,11 @@ * To change this template use Tools | Options | Coding | Edit Standard Headers. */ +using System; using System.ComponentModel; -using System.IO; +using System.Windows.Forms; +using Microsoft.Extensions.Logging; +using JexusManager; namespace JexusManager.Features.HttpApi { @@ -37,6 +40,8 @@ namespace JexusManager.Features.HttpApi /// internal class SniMappingFeature : HttpApiFeature { + private static readonly ILogger _logger = LogHelper.GetLogger("SniMappingFeature"); + private sealed class FeatureTaskList : DefaultTaskList { private readonly SniMappingFeature _owner; @@ -110,6 +115,11 @@ public void Remove() return; } + DeleteMapping(); + } + + private void DeleteMapping() + { try { // remove certificate and mapping @@ -135,21 +145,14 @@ public void Remove() catch (Win32Exception ex) { // elevation is cancelled. - var message = NativeMethods.KnownCases(ex.NativeErrorCode); - if (string.IsNullOrEmpty(message)) - { - Debug.WriteLine(ex); - Debug.WriteLine($"native {ex.NativeErrorCode}"); - // throw; - } - else + if (ex.NativeErrorCode != (int)Windows.Win32.Foundation.WIN32_ERROR.ERROR_CANCELLED) { - dialog.ShowError(ex, message, Name, false); + _logger.LogError(ex, "Win32 error deleting SNI mapping. Native error code: {Code}", ex.NativeErrorCode); } } catch (Exception ex) { - Debug.WriteLine(ex); + _logger.LogError(ex, "Error deleting SNI mapping"); } } diff --git a/JexusManager.Shared/BindingUtility.cs b/JexusManager.Shared/BindingUtility.cs index 59fff578..4524c3fa 100644 --- a/JexusManager.Shared/BindingUtility.cs +++ b/JexusManager.Shared/BindingUtility.cs @@ -15,7 +15,7 @@ namespace Microsoft.Web.Administration using System.Net; using System.Security.Cryptography.X509Certificates; using System.Windows.Forms; - + using Microsoft.Extensions.Logging; using JexusManager; using Org.BouncyCastle.Utilities.Encoders; using static System.Windows.Forms.VisualStyles.VisualStyleElement.Window; @@ -35,6 +35,8 @@ public enum CertificateMappingState public static class BindingUtility { + private static readonly ILogger _logger = LogHelper.GetLogger("BindingUtility"); + private const string AppIdIisExpress = "214124cd-d05b-4309-9af9-9caa44b2b74a"; private const string AppIdIis = "4dc3e181-e14b-4a21-b022-59fc669b0914"; @@ -156,8 +158,7 @@ private static (CertificateMappingState state, string message) ManipulateCertifi var message = NativeMethods.KnownCases(ex.NativeErrorCode); if (string.IsNullOrEmpty(message)) { - Debug.WriteLine(ex); - Debug.WriteLine($"native {ex.NativeErrorCode}"); + _logger.LogWarning(ex, "Win32 error during certificate mapping. Native error code: {Code}", ex.NativeErrorCode); return (CertificateMappingState.Win32ErrorOccurred, $"Register new certificate failed: unknown (native {ex.NativeErrorCode})"); } @@ -165,7 +166,7 @@ private static (CertificateMappingState state, string message) ManipulateCertifi } catch (Exception ex) { - Debug.WriteLine(ex); + _logger.LogError(ex, "Unhandled error during certificate mapping"); return (CertificateMappingState.GenericErrorOccurred, $"Register new certificate failed: unknown ({ex.Message})"); } } @@ -275,8 +276,7 @@ private static string RemoveMapping(this Binding binding, bool sni) var message = NativeMethods.KnownCases(ex.NativeErrorCode); if (string.IsNullOrEmpty(message)) { - Debug.WriteLine(ex); - Debug.WriteLine($"native {ex.NativeErrorCode}"); + _logger.LogWarning(ex, "Win32 error during mapping removal. Native error code: {Code}", ex.NativeErrorCode); return $"Remove SNI certificate failed: unknown (native {ex.NativeErrorCode})"; } @@ -284,13 +284,13 @@ private static string RemoveMapping(this Binding binding, bool sni) } catch (NullReferenceException ex) { - Debug.WriteLine(ex); - Debug.WriteLine($"binding {binding.ToString()} endpointNull {binding.EndPoint == null}"); + _logger.LogError(ex, "Null reference during mapping removal. Binding: {Binding}, EndPoint null: {IsNull}", + binding.ToString(), binding.EndPoint == null); return $"Remove SNI certificate failed: unknown ({ex.Message})"; } catch (Exception ex) { - Debug.WriteLine(ex); + _logger.LogError(ex, "Unhandled error during mapping removal"); return $"Remove SNI certificate failed: unknown ({ex.Message})"; } } @@ -332,8 +332,7 @@ public static string AddReservedUrl(string url) var message = NativeMethods.KnownCases(ex.NativeErrorCode); if (string.IsNullOrEmpty(message)) { - Debug.WriteLine(ex); - Debug.WriteLine($"native {ex.NativeErrorCode}"); + _logger.LogWarning(ex, "Win32 error adding reserved URL. Native error code: {Code}", ex.NativeErrorCode); return $"failed to add reserved URL: unknown ({message})"; } else @@ -343,7 +342,7 @@ public static string AddReservedUrl(string url) } catch (Exception ex) { - Debug.WriteLine(ex); + _logger.LogError(ex, "Error adding reserved URL: {Url}", url); return $"failed to add reserved URL: unknown ({ex.Message})"; } } diff --git a/JexusManager.Shared/DialogHelper.cs b/JexusManager.Shared/DialogHelper.cs index 4d604e65..c37be0f9 100644 --- a/JexusManager.Shared/DialogHelper.cs +++ b/JexusManager.Shared/DialogHelper.cs @@ -9,18 +9,21 @@ namespace JexusManager using System; using System.ComponentModel; using System.Diagnostics; - using System.Drawing; using System.IO; using System.Linq; - using System.Net; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Windows.Forms; + using Microsoft.Extensions.Logging; using Tulpep.NotificationWindow; + using System.Net; + using System.Drawing; public static class DialogHelper { + private static readonly ILogger _logger = LogHelper.GetLogger("DialogHelper"); + public static string[] Conditions = new[] { "ALL_HTTP", @@ -274,7 +277,7 @@ public static void LoadCertificates(ComboBox comboBox, byte[] hash, string store { if (ex.HResult != Microsoft.Web.Administration.NativeMethods.NonExistingStore) { - Debug.WriteLine($"CryptographicException {ex.HResult} from LoadCertificates MY"); + _logger.LogWarning(ex, "Error accessing MY certificate store. HResult: {HResult}", ex.HResult); throw; } } @@ -307,7 +310,7 @@ public static void LoadCertificates(ComboBox comboBox, byte[] hash, string store { if (ex.HResult != Microsoft.Web.Administration.NativeMethods.NonExistingStore) { - Debug.WriteLine($"CryptographicException {ex.HResult} from LoadCertificates WebHosting"); + _logger.LogWarning(ex, "Error accessing WebHosting certificate store. HResult: {HResult}", ex.HResult); throw; } } @@ -343,7 +346,7 @@ private static string GetSpecialFolder(string name, string file) } catch (IOException ex) { - Debug.WriteLine(ex); + _logger.LogError(ex, "Error creating directory {Path}", result); } } @@ -393,7 +396,7 @@ public static void BrowseFile(string file) return; } - MessageBoxShow($"Windows Explorer cannot be located.", true); + _logger.LogWarning("Windows Explorer not found at {Path}", path); } public static void SiteStart(Site site) diff --git a/JexusManager.Shared/Features/TaskItemExtensions.cs b/JexusManager.Shared/Features/TaskItemExtensions.cs index d5283eaa..68989b56 100644 --- a/JexusManager.Shared/Features/TaskItemExtensions.cs +++ b/JexusManager.Shared/Features/TaskItemExtensions.cs @@ -4,21 +4,20 @@ using System; using System.Reflection; - -namespace JexusManager.Features +using Microsoft.Extensions.Logging; +using JexusManager; +using Microsoft.Web.Management.Client.Win32; +using System.Windows.Forms; +using System.Collections; +using JexusManager.Properties; +using System.Drawing; + +namespace Microsoft.Web.Management.Client { - using System.Collections; - using System.Diagnostics; - using System.Drawing; - using System.Windows.Forms; - - using JexusManager.Properties; - - using Microsoft.Web.Management.Client; - using Microsoft.Web.Management.Client.Win32; - public static class TaskItemExtensions { + private static readonly ILogger _logger = LogHelper.GetLogger("TaskItemExtensions"); + public static void Fill(this TaskListCollection tasks, ToolStrip actionPanel, ContextMenuStrip actionMenu, TaskList extra = null) { actionPanel.Items.Clear(); @@ -91,11 +90,11 @@ private static void TaskItemFill(this TaskItem item, TaskList list, ToolStrip ac list.InvokeMethod(method.MethodName, method.UserData); } catch (TargetInvocationException ex) - { + { if (ex.InnerException is UnauthorizedAccessException) { + _logger.LogError(ex.InnerException, "Error invoking task method {Method}", method.MethodName); MessageBox.Show($"{ex.InnerException.Message}. Running Jexus Manager as administrator might resolve it.", "Jexus Manager", MessageBoxButtons.OK, MessageBoxIcon.Error); - Debug.WriteLine(ex.InnerException); return; } @@ -120,11 +119,11 @@ private static void TaskItemFill(this TaskItem item, TaskList list, ToolStrip ac list.InvokeMethod(method.MethodName, method.UserData); } catch (TargetInvocationException ex) - { + { if (ex.InnerException is UnauthorizedAccessException) { + _logger.LogError(ex.InnerException, "Error invoking task method {Method}", method.MethodName); MessageBox.Show($"{ex.InnerException.Message}. Running Jexus Manager as administrator might resolve it.", "Jexus Manager", MessageBoxButtons.OK, MessageBoxIcon.Error); - Debug.WriteLine(ex.InnerException); return; } diff --git a/JexusManager/Features/Main/BindingDiagDialog.cs b/JexusManager/Features/Main/BindingDiagDialog.cs index 4269af75..69a39315 100644 --- a/JexusManager/Features/Main/BindingDiagDialog.cs +++ b/JexusManager/Features/Main/BindingDiagDialog.cs @@ -7,24 +7,25 @@ using System; using System.Linq; using System.Security.Cryptography; +using Microsoft.Extensions.Logging; +using JexusManager; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.IO; +using System.Drawing; +using Microsoft.Win32; +using EnumsNET; +using System.Net; +using System.Net.Sockets; +using JexusManager.Features.HttpApi; +using Microsoft.Web.Management.Client; namespace JexusManager.Features.Main { - using System.Drawing; - using System.IO; - using Microsoft.Win32; - - using System.Reactive.Disposables; - using System.Reactive.Linq; - using System.Collections.Generic; - using System.Net; - using System.Net.Sockets; - using EnumsNET; - using JexusManager.Features.HttpApi; - using Microsoft.Web.Management.Client; - public partial class BindingDiagDialog : DialogForm { + private static readonly ILogger _logger = LogHelper.GetLogger("BindingDiagDialog"); + public BindingDiagDialog(IServiceProvider provider, Site site) : base(provider) { @@ -39,197 +40,7 @@ public BindingDiagDialog(IServiceProvider provider, Site site) .Subscribe(evt => { txtResult.Clear(); - try - { - Warn("IMPORTANT: This report might contain confidential information. Mask such before sharing with others."); - Warn("-----"); - Debug($"System Time: {DateTime.Now}"); - Debug($"Processor Architecture: {Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")}"); - Debug($"OS: {Environment.OSVersion}"); - Debug($"Server Type: {site.Server.Mode.AsString(EnumFormat.Description)}"); - Debug("-----"); - - var adapters = Dns.GetHostEntry(string.Empty).AddressList.Where(address => !address.IsIPv6LinkLocal).ToList(); - if (adapters.Count == 0) - { - Warn("This machine has no suitable IP address to accept external traffic."); - } - else - { - Info($"This machine has {adapters.Count} IP addresses to take external traffic."); - foreach (IPAddress address in adapters) - { - Info(address.AddressFamily == AddressFamily.InterNetworkV6 - ? $"* [{address}]." - : $"* {address}."); - } - } - - Debug("-----"); - - Debug($"[W3SVC/{site.Id}]"); - Debug($"ServerComment : {site.Name}"); - Debug($"ServerAutoStart: {site.ServerAutoStart}"); - Debug($"ServerState : {site.State}"); - Debug(string.Empty); - var feature = new ReservedUrlsFeature((Module)provider); - feature.Load(); - foreach (Binding binding in site.Bindings) - { - Debug($"BINDING: {binding.Protocol} {binding}"); - if (binding.Protocol != "https" && binding.Protocol != "http") - { - Warn("This prototol is not analyzed."); - Debug(string.Empty); - continue; - } - - if (PublicNativeMethods.IsProcessElevated) - { - var found = Microsoft.Web.Administration.NativeMethods.FoundReserved((ushort)binding.EndPoint.Port); - if (found) - { - Error("Found a conflicting TCP reserved port range. Please run \"netsh int ipv4 show excludedportrange protocol=tcp\" at command prompt to troubleshoot."); - } - else - { - Info("No conflicting TCP reserved port range is found."); - } - } - else - { - Warn("Jexus Manager is not running as administrator, so TCP reserved port range is not verified. Please run \"netsh int ipv4 show excludedportrange protocol=tcp\" at command prompt to see if any conflict exists."); - } - - if (binding.Host != "localhost") - { - if (site.Server.Mode == WorkingMode.IisExpress) - { - var reservation = binding.ToUrlPrefix(); - if (!feature.Items.Any(item => item.UrlPrefix == reservation)) - { - Warn($"URL reservation {reservation} is missing. So this binding only works if IIS Express runs as administrator."); - } - } - - Info($"This site can take external traffic if,"); - Info($" * TCP port {binding.EndPoint.Port} must be opened on Windows Firewall (or any other installed firewall products)."); - } - - if (binding.EndPoint.Address.Equals(IPAddress.Any)) - { - if (binding.Host != "localhost") - { - Info($" * Requests from web browsers must be routed to following end points on this machine,"); - foreach (IPAddress address in adapters) - { - Info(address.AddressFamily == AddressFamily.InterNetworkV6 - ? $" * [{address}]:{binding.EndPoint.Port}." - : $" * {address}:{binding.EndPoint.Port}."); - } - } - - if (Socket.OSSupportsIPv4) - { - Debug($"This site can take local traffic at {IPAddress.Loopback}:{binding.EndPoint.Port}."); - } - - if (Socket.OSSupportsIPv6) - { - Debug($"This site can take local traffic at [{IPAddress.IPv6Loopback}]:{binding.EndPoint.Port}."); - } - } - else - { - Info($" * The networking must be properly set up to forward requests from web browsers to {binding.EndPoint} on this machine."); - } - - if (binding.Host == "*" || binding.Host == string.Empty) - { - if (binding.EndPoint.Address.Equals(IPAddress.Any)) - { - Info($" * Web browsers can use several URLs, such as"); - foreach (IPAddress address in adapters) - { - Debug(address.AddressFamily == AddressFamily.InterNetworkV6 - ? $" * {binding.Protocol}://[{address}]:{binding.EndPoint.Port}." - : $" * {binding.Protocol}://{address}:{binding.EndPoint.Port}."); - } - - Info($" * {binding.Protocol}://localhost:{binding.EndPoint.Port}."); - Info($" * {binding.Protocol}://{IPAddress.Loopback}:{binding.EndPoint.Port}."); - Info($" * {binding.Protocol}://[{IPAddress.IPv6Loopback}]:{binding.EndPoint.Port}."); - } - else - { - Info($" * Web browsers should use URL {binding.Protocol}://{binding.EndPoint.Address}:{binding.EndPoint.Port}."); - } - } - else - { - Info($" * Web browsers should use URL {binding.Protocol}://{binding.Host}:{binding.EndPoint.Port}. Requests must have Host header of \"{binding.Host}\"."); - if (!binding.Host.IsWildcard()) - { - Info($" Start DNS query for {binding.Host}."); - // IMPORTANT: wildcard host is not supported. - try - { - var entry = Dns.GetHostEntry(binding.Host); - var list = entry.AddressList; - Info($" DNS Query returns {list.Length} result(s)."); - var found = false; - foreach (var address in list) - { - Info(address.AddressFamily == AddressFamily.InterNetworkV6 - ? $" * [{address}]" - : $" * {address}"); - if (adapters.Any(item => address.Equals(item))) - { - found = true; - break; - } - - if (address.Equals(IPAddress.Loopback)) - { - found = true; - } - } - - if (!found) - { - Warn($" DNS query of \"{binding.Host}\" does not return a known IP address for any network adapter of this machine."); - Warn(" The server usually uses private IP addresses, and DNS query returns public IP addresses."); - Warn(" If packets are forwarded from public IP to private IP properly, this warning can be ignored."); - Warn(" Otherwise, please review DNS settings (or modify the hosts file to emulate DNS)."); - } - } - catch (SocketException ex) - { - Error($"DNS query failed: {ex.Message}."); - Error($"Please review the host name {binding.Host}."); - } - } - } - - if (binding.Protocol == "https") - { - Warn("Binding Diagnostics does not verify certificates and other SSL/TLS related settings."); - Warn($"Please run SSL Diagnostics at server level to analyze SSL/TLS configuration. More information can be found at https://docs.jexusmanager.com/tutorials/ssl-diagnostics.html."); - } - - Debug(string.Empty); - } - } - catch (CryptographicException ex) - { - Debug(ex.ToString()); - System.Diagnostics.Debug.WriteLine(ex); - System.Diagnostics.Debug.WriteLine($"hResult {ex.HResult}"); - } - catch (Exception ex) - { - Debug(ex.ToString()); - } + AnalyzeBinding(site, (Module)provider); })); container.Add( @@ -328,5 +139,198 @@ private void BindingDiagDialogHelpButtonClicked(object sender, System.ComponentM { BtnHelpClick(null, EventArgs.Empty); } + + private void AnalyzeBinding(Site site, Module provider) + { + try + { + Warn("IMPORTANT: This report might contain confidential information. Mask such before sharing with others."); + Warn("-----"); + Debug($"System Time: {DateTime.Now}"); + Debug($"Processor Architecture: {Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")}"); + Debug($"OS: {Environment.OSVersion}"); + Debug($"Server Type: {site.Server.Mode.AsString(EnumFormat.Description)}"); + Debug("-----"); + + var adapters = Dns.GetHostEntry(string.Empty).AddressList.Where(address => !address.IsIPv6LinkLocal).ToList(); + if (adapters.Count == 0) + { + Warn("This machine has no suitable IP address to accept external traffic."); + } + else + { + Info($"This machine has {adapters.Count} IP addresses to take external traffic."); + foreach (IPAddress address in adapters) + { + Info(address.AddressFamily == AddressFamily.InterNetworkV6 + ? $"* [{address}]." + : $"* {address}."); + } + } + + Debug("-----"); + + Debug($"[W3SVC/{site.Id}]"); + Debug($"ServerComment : {site.Name}"); + Debug($"ServerAutoStart: {site.ServerAutoStart}"); + Debug($"ServerState : {site.State}"); + Debug(string.Empty); + var feature = new ReservedUrlsFeature(provider); + feature.Load(); + foreach (Binding binding in site.Bindings) + { + Debug($"BINDING: {binding.Protocol} {binding}"); + if (binding.Protocol != "https" && binding.Protocol != "http") + { + Warn("This prototol is not analyzed."); + Debug(string.Empty); + continue; + } + + if (PublicNativeMethods.IsProcessElevated) + { + var found = Microsoft.Web.Administration.NativeMethods.FoundReserved((ushort)binding.EndPoint.Port); + if (found) + { + Error("Found a conflicting TCP reserved port range. Please run \"netsh int ipv4 show excludedportrange protocol=tcp\" at command prompt to troubleshoot."); + } + else + { + Info("No conflicting TCP reserved port range is found."); + } + } + else + { + Warn("Jexus Manager is not running as administrator, so TCP reserved port range is not verified. Please run \"netsh int ipv4 show excludedportrange protocol=tcp\" at command prompt to see if any conflict exists."); + } + + if (binding.Host != "localhost") + { + if (site.Server.Mode == WorkingMode.IisExpress) + { + var reservation = binding.ToUrlPrefix(); + if (!feature.Items.Any(item => item.UrlPrefix == reservation)) + { + Warn($"URL reservation {reservation} is missing. So this binding only works if IIS Express runs as administrator."); + } + } + + Info($"This site can take external traffic if,"); + Info($" * TCP port {binding.EndPoint.Port} must be opened on Windows Firewall (or any other installed firewall products)."); + } + + if (binding.EndPoint.Address.Equals(IPAddress.Any)) + { + if (binding.Host != "localhost") + { + Info($" * Requests from web browsers must be routed to following end points on this machine,"); + foreach (IPAddress address in adapters) + { + Info(address.AddressFamily == AddressFamily.InterNetworkV6 + ? $" * [{address}]:{binding.EndPoint.Port}." + : $" * {address}:{binding.EndPoint.Port}."); + } + } + + if (Socket.OSSupportsIPv4) + { + Debug($"This site can take local traffic at {IPAddress.Loopback}:{binding.EndPoint.Port}."); + } + + if (Socket.OSSupportsIPv6) + { + Debug($"This site can take local traffic at [{IPAddress.IPv6Loopback}]:{binding.EndPoint.Port}."); + } + } + else + { + Info($" * The networking must be properly set up to forward requests from web browsers to {binding.EndPoint} on this machine."); + } + + if (binding.Host == "*" || binding.Host == string.Empty) + { + if (binding.EndPoint.Address.Equals(IPAddress.Any)) + { + Info($" * Web browsers can use several URLs, such as"); + foreach (IPAddress address in adapters) + { + Debug(address.AddressFamily == AddressFamily.InterNetworkV6 + ? $" * {binding.Protocol}://[{address}]:{binding.EndPoint.Port}." + : $" * {binding.Protocol}://{address}:{binding.EndPoint.Port}."); + } + + Info($" * {binding.Protocol}://localhost:{binding.EndPoint.Port}."); + Info($" * {binding.Protocol}://{IPAddress.Loopback}:{binding.EndPoint.Port}."); + Info($" * {binding.Protocol}://[{IPAddress.IPv6Loopback}]:{binding.EndPoint.Port}."); + } + else + { + Info($" * Web browsers should use URL {binding.Protocol}://{binding.EndPoint.Address}:{binding.EndPoint.Port}."); + } + } + else + { + Info($" * Web browsers should use URL {binding.Protocol}://{binding.Host}:{binding.EndPoint.Port}. Requests must have Host header of \"{binding.Host}\"."); + if (!binding.Host.IsWildcard()) + { + Info($" Start DNS query for {binding.Host}."); + // IMPORTANT: wildcard host is not supported. + try + { + var entry = Dns.GetHostEntry(binding.Host); + var list = entry.AddressList; + Info($" DNS Query returns {list.Length} result(s)."); + var found = false; + foreach (var address in list) + { + Info(address.AddressFamily == AddressFamily.InterNetworkV6 + ? $" * [{address}]" + : $" * {address}"); + if (adapters.Any(item => address.Equals(item))) + { + found = true; + break; + } + + if (address.Equals(IPAddress.Loopback)) + { + found = true; + } + } + + if (!found) + { + Warn($" DNS query of \"{binding.Host}\" does not return a known IP address for any network adapter of this machine."); + Warn(" The server usually uses private IP addresses, and DNS query returns public IP addresses."); + Warn(" If packets are forwarded from public IP to private IP properly, this warning can be ignored."); + Warn(" Otherwise, please review DNS settings (or modify the hosts file to emulate DNS)."); + } + } + catch (SocketException ex) + { + Error($"DNS query failed: {ex.Message}."); + Error($"Please review the host name {binding.Host}."); + } + } + } + + if (binding.Protocol == "https") + { + Warn("Binding Diagnostics does not verify certificates and other SSL/TLS related settings."); + Warn($"Please run SSL Diagnostics at server level to analyze SSL/TLS configuration. More information can be found at https://docs.jexusmanager.com/tutorials/ssl-diagnostics.html."); + } + + Debug(string.Empty); + } + } + catch (CryptographicException ex) + { + _logger.LogError(ex, "Cryptographic error in binding diagnostics. HResult: {HResult}", ex.HResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error analyzing binding"); + } + } } } diff --git a/JexusManager/Features/Main/HomePage.Designer.cs b/JexusManager/Features/Main/HomePage.Designer.cs index d84fd497..9a68e95f 100644 --- a/JexusManager/Features/Main/HomePage.Designer.cs +++ b/JexusManager/Features/Main/HomePage.Designer.cs @@ -35,6 +35,10 @@ private void InitializeComponent() this.label2 = new System.Windows.Forms.Label(); this.label1 = new System.Windows.Forms.Label(); this.panel2 = new System.Windows.Forms.Panel(); + this.groupBox2 = new System.Windows.Forms.GroupBox(); + this.lblCurrentVersion = new System.Windows.Forms.Label(); + this.lblUpdateStatus = new System.Windows.Forms.Label(); + this.btnDownloadUpdate = new System.Windows.Forms.Button(); this.label3 = new System.Windows.Forms.Label(); this.panel3 = new System.Windows.Forms.Panel(); this.groupBox1 = new System.Windows.Forms.GroupBox(); @@ -43,6 +47,7 @@ private void InitializeComponent() this.btnSponsor = new System.Windows.Forms.Button(); this.panel1.SuspendLayout(); this.panel2.SuspendLayout(); + this.groupBox2.SuspendLayout(); this.groupBox1.SuspendLayout(); this.SuspendLayout(); // @@ -82,6 +87,7 @@ private void InitializeComponent() // panel2 // this.panel2.BackColor = System.Drawing.Color.White; + this.panel2.Controls.Add(this.groupBox2); this.panel2.Controls.Add(this.label3); this.panel2.Controls.Add(this.panel3); this.panel2.Controls.Add(this.groupBox1); @@ -92,6 +98,51 @@ private void InitializeComponent() this.panel2.Size = new System.Drawing.Size(862, 489); this.panel2.TabIndex = 1; // + // groupBox2 + // + this.groupBox2.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.groupBox2.Controls.Add(this.lblCurrentVersion); + this.groupBox2.Controls.Add(this.lblUpdateStatus); + this.groupBox2.Controls.Add(this.btnDownloadUpdate); + this.groupBox2.Location = new System.Drawing.Point(360, 32); + this.groupBox2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + this.groupBox2.Name = "groupBox2"; + this.groupBox2.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3); + this.groupBox2.Size = new System.Drawing.Size(478, 160); + this.groupBox2.TabIndex = 3; + this.groupBox2.TabStop = false; + this.groupBox2.Text = "Updates"; + // + // lblCurrentVersion + // + this.lblCurrentVersion.AutoSize = true; + this.lblCurrentVersion.Location = new System.Drawing.Point(24, 33); + this.lblCurrentVersion.Name = "lblCurrentVersion"; + this.lblCurrentVersion.Size = new System.Drawing.Size(92, 15); + this.lblCurrentVersion.TabIndex = 0; + this.lblCurrentVersion.Text = "Current Version:"; + // + // lblUpdateStatus + // + this.lblUpdateStatus.AutoSize = true; + this.lblUpdateStatus.Location = new System.Drawing.Point(24, 76); + this.lblUpdateStatus.Name = "lblUpdateStatus"; + this.lblUpdateStatus.Size = new System.Drawing.Size(153, 15); + this.lblUpdateStatus.TabIndex = 1; + this.lblUpdateStatus.Text = "Checking for updates..."; + // + // btnDownloadUpdate + // + this.btnDownloadUpdate.Location = new System.Drawing.Point(24, 110); + this.btnDownloadUpdate.Name = "btnDownloadUpdate"; + this.btnDownloadUpdate.Size = new System.Drawing.Size(200, 30); + this.btnDownloadUpdate.TabIndex = 2; + this.btnDownloadUpdate.Text = "Download Update"; + this.btnDownloadUpdate.UseVisualStyleBackColor = true; + this.btnDownloadUpdate.Visible = false; + this.btnDownloadUpdate.Click += new System.EventHandler(this.btnDownloadUpdate_Click); + // // label3 // this.label3.AutoSize = true; @@ -175,6 +226,8 @@ private void InitializeComponent() this.panel1.PerformLayout(); this.panel2.ResumeLayout(false); this.panel2.PerformLayout(); + this.groupBox2.ResumeLayout(false); + this.groupBox2.PerformLayout(); this.groupBox1.ResumeLayout(false); this.groupBox1.PerformLayout(); this.ResumeLayout(false); @@ -193,5 +246,9 @@ private void InitializeComponent() private Label label3; private LinkLabel txtStudio; private Button btnSponsor; + private GroupBox groupBox2; + private Label lblCurrentVersion; + private Label lblUpdateStatus; + private Button btnDownloadUpdate; } } diff --git a/JexusManager/Features/Main/HomePage.cs b/JexusManager/Features/Main/HomePage.cs index cccce634..b73426d1 100644 --- a/JexusManager/Features/Main/HomePage.cs +++ b/JexusManager/Features/Main/HomePage.cs @@ -4,16 +4,66 @@ namespace JexusManager.Features.Main { + using System; using System.Diagnostics; + using System.Drawing; + using System.Threading.Tasks; using System.Windows.Forms; using Microsoft.Web.Management.Client.Win32; public partial class HomePage : ModulePage { + private UpdateHelper.UpdateInfo _updateInfo; + public HomePage() { InitializeComponent(); + LoadUpdateInfo(); + } + + private async void LoadUpdateInfo() + { + try + { + _updateInfo = await UpdateHelper.CheckForUpdate(); + UpdateVersionDisplay(); + } + catch (Exception ex) + { + lblUpdateStatus.Text = $"Error checking for updates: {ex.Message}"; + lblUpdateStatus.ForeColor = Color.Red; + } + } + + private void UpdateVersionDisplay() + { + if (_updateInfo == null) + return; + + // Always show current version + lblCurrentVersion.Text = $"Current Version: {_updateInfo.CurrentVersion}"; + + if (!string.IsNullOrEmpty(_updateInfo.ErrorMessage)) + { + lblUpdateStatus.Text = _updateInfo.ErrorMessage; + lblUpdateStatus.ForeColor = Color.Red; + btnDownloadUpdate.Visible = false; + return; + } + + if (_updateInfo.UpdateAvailable) + { + lblUpdateStatus.Text = $"An update is available: {_updateInfo.LatestVersion}"; + lblUpdateStatus.ForeColor = Color.Green; + btnDownloadUpdate.Visible = true; + } + else + { + lblUpdateStatus.Text = "You are using the latest version."; + lblUpdateStatus.ForeColor = Color.Green; + btnDownloadUpdate.Visible = false; + } } private void txtHome_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) @@ -25,12 +75,20 @@ private void txtHome_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) private void txtStudio_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { - DialogHelper.ProcessStart("https://blog.lextudio.com"); + DialogHelper.ProcessStart("https://halfblood.pro"); } private void btnSponsor_Click(object sender, System.EventArgs e) { DialogHelper.ProcessStart("https://github.com/sponsors/lextm"); } + + private void btnDownloadUpdate_Click(object sender, EventArgs e) + { + if (_updateInfo != null && !string.IsNullOrEmpty(_updateInfo.ReleaseUrl)) + { + DialogHelper.ProcessStart(_updateInfo.ReleaseUrl); + } + } } } diff --git a/JexusManager/Features/Main/KestrelDiagDialog.cs b/JexusManager/Features/Main/KestrelDiagDialog.cs index 2029ea0d..44ac64f3 100644 --- a/JexusManager/Features/Main/KestrelDiagDialog.cs +++ b/JexusManager/Features/Main/KestrelDiagDialog.cs @@ -26,9 +26,11 @@ namespace JexusManager.Features.Main using Newtonsoft.Json; using System.Net; using NuGet.Versioning; + using Microsoft.Extensions.Logging; public partial class KestrelDiagDialog : DialogForm { + private static readonly ILogger _logger = LogHelper.GetLogger("KestrelDiagDialog"); private static IDictionary> mappings = new Dictionary>(); private static IDictionary fileCaches = new Dictionary(); @@ -488,8 +490,7 @@ public KestrelDiagDialog(IServiceProvider provider, Application application) catch (Exception ex) { Error("Cannot analyze ASP.NET Core web app successfully."); - System.Diagnostics.Debug.WriteLine(ex); - System.Diagnostics.Debug.WriteLine("source web app"); + _logger.LogError(ex, "Error loading web.config of source web app"); } } } @@ -498,12 +499,12 @@ public KestrelDiagDialog(IServiceProvider provider, Application application) Error("A generic exception occurred."); Error($"To run ASP.NET Core on IIS, please refer to https://docs.microsoft.com/aspnet/core/host-and-deploy/iis/index for more details."); Debug(ex.ToString()); - System.Diagnostics.Debug.WriteLine(ex); + _logger.LogDebug(ex.ToString()); } catch (Exception ex) { Debug(ex.ToString()); - System.Diagnostics.Debug.WriteLine(ex); + _logger.LogDebug(ex.ToString()); } })); diff --git a/JexusManager/Features/Main/PhpDiagDialog.cs b/JexusManager/Features/Main/PhpDiagDialog.cs index a3746058..e1673549 100644 --- a/JexusManager/Features/Main/PhpDiagDialog.cs +++ b/JexusManager/Features/Main/PhpDiagDialog.cs @@ -22,9 +22,14 @@ namespace JexusManager.Features.Main using IniParser.Parser; using EnumsNET; using JexusManager.Features.Modules; + using System.Windows.Forms; + using Microsoft.Extensions.Logging; + using JexusManager; public partial class PhpDiagDialog : DialogForm { + private static readonly ILogger _logger = LogHelper.GetLogger("PhpDiagDialog"); + private struct PhpVersion { public PhpVersion(string version, DateTime expiringDate, Version cppVersion) @@ -65,234 +70,7 @@ public PhpDiagDialog(IServiceProvider provider, ServerManager server) .Subscribe(evt => { txtResult.Clear(); - try - { - Warn("IMPORTANT: This report might contain confidential information. Mask such before sharing with others."); - Warn("-----"); - Debug($"System Time: {DateTime.Now}"); - Debug($"Processor Architecture: {Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")}"); - Debug($"OS: {Environment.OSVersion}"); - Debug($"Server Type: {server.Mode.AsString(EnumFormat.Description)}"); - Debug(string.Empty); - - var modules = new ModulesFeature((Module)provider); - modules.Load(); - Debug($"Scan {modules.Items.Count} installed module(s)."); - if (modules.Items.All(item => item.Name != "FastCgiModule")) - { - Error($"FastCGI module is not installed as part of IIS. Please refer to https://learn.microsoft.com/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/hh994590(v=ws.11) for more details."); - return; - } - else - { - Debug("FastCGI module is installed."); - } - - Debug(string.Empty); - var handlers = new HandlersFeature((Module)provider); - handlers.Load(); - var foundPhpHandler = new List(); - Debug($"Scan {handlers.Items.Count} registered handler(s)."); - foreach (var item in handlers.Items) - { - if (item.Modules == "FastCgiModule") - { - if (File.Exists(item.ScriptProcessor)) - { - Debug($"* Found a valid FastCGI handler as {{ Name: {item.Name}, Path: {item.Path}, State: {item.GetState(handlers.AccessPolicy)}, Module: {item.TypeString}, Entry Type: {item.Flag} }}. File path: {item.ScriptProcessor}."); - foundPhpHandler.Add(item); - } - else - { - Error($"* Found an invalid FastCGI handler as {{Name: {item.Name}, Path: {item.Path}, State: {item.GetState(handlers.AccessPolicy)}, Module: {item.TypeString}, Entry Type: {item.Flag} }} because file path does not exist: {item.ScriptProcessor}."); - } - } - } - - if (foundPhpHandler.Count == 0) - { - Error($"No valid FastCGI handler is registered for this web site."); - Error($"To run PHP on IIS, please refer to https://learn.microsoft.com/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/hh994590(v=ws.11) for more details."); - return; - } - - Debug(string.Empty); - var fastCgiFeature = new FastCgiFeature((Module)provider); - fastCgiFeature.Load(); - Debug($"Scan {fastCgiFeature.Items.Count} registered FastCGI application(s)."); - var foundPhp = new List(); - foreach (var item in fastCgiFeature.Items) - { - var combination = string.IsNullOrWhiteSpace(item.Arguments) ? item.Path : item.Path + '|' + item.Arguments; - foreach (var handler in foundPhpHandler) - { - if (string.Equals(combination, handler.ScriptProcessor, StringComparison.OrdinalIgnoreCase)) - { - Debug($"* Found FastCGI application registered as {{ Full path: {item.Path}, Arguments: {item.Arguments} }}."); - foundPhp.Add(item); - break; - } - } - } - - if (foundPhp.Count == 0) - { - Error($"No suitable FastCGI appilcation is registered on this server."); - Error($"To run PHP on IIS, please refer to https://learn.microsoft.com/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/hh994590(v=ws.11) for more details."); - return; - } - - Debug(Environment.NewLine); - Debug($"Verify web stack installation versions."); - foreach (var item in foundPhp) - { - var path = item.Path; - if (path.TrimEnd('"').EndsWith("php-cgi.exe", StringComparison.OrdinalIgnoreCase)) - { - // PHP - var info = FileVersionInfo.GetVersionInfo(path); - var version = $"{info.FileMajorPart}.{info.FileMinorPart}"; - if (knownPhpVersions.ContainsKey(version)) - { - var matched = knownPhpVersions[version]; - if (matched.ExpiringDate <= DateTime.Now) - { - Error($"* PHP {info.FileVersion} ({path}) is unknown or obsolete. Please refer to http://php.net/supported-versions.php for more details."); - } - else if (matched.ExpiringDate > DateTime.Now && (matched.ExpiringDate - DateTime.Now).TotalDays < 180) - { - Warn($"* PHP {version} ({path}) will soon be obsolete. Please refer to http://php.net/supported-versions.php for more details."); - } - else - { - Debug($"* PHP {version} ({path}) is supported."); - } - - var x86 = DialogHelper.GetImageArchitecture(path); - var cppFile = Path.Combine( - Environment.GetFolderPath(x86 ? Environment.SpecialFolder.SystemX86 : Environment.SpecialFolder.System), - $"msvcp{matched.CppVersion.Major}0.dll"); - if (File.Exists(cppFile)) - { - var cpp = FileVersionInfo.GetVersionInfo(cppFile); - if (cpp.FileMinorPart >= matched.CppVersion.Minor) - { - Debug($" Visual C++ runtime is detected (expected: {matched.CppVersion}, detected: {cpp.FileVersion}): {cppFile}."); - } - else - { - Error($" Visual C++ runtime {matched.CppVersion} is not detected. Please install it following the tips on https://windows.php.net/download/."); - } - } - else - { - Error($" Visual C++ {matched.CppVersion} runtime is not detected. Please install it following the tips on https://windows.php.net/download/."); - } - } - else - { - Error($"* PHP {info.FileVersion} ({path}) is unknown or obsolete. Please refer to http://php.net/supported-versions.php for more details."); - } - } - } - - Debug(string.Empty); - var systemPath = Environment.GetEnvironmentVariable("Path"); - Debug($"Windows Path environment variable: {systemPath}."); - Debug(string.Empty); - string[] paths = systemPath.Split(new char[1] { Path.PathSeparator }); - foreach (var item in foundPhp) - { - var path = item.Path; - if (path.TrimEnd('"').EndsWith("php-cgi.exe", StringComparison.OrdinalIgnoreCase)) - { - var rootFolder = Path.GetDirectoryName(path); - Debug($"[{rootFolder}]"); - if (!Directory.Exists(rootFolder)) - { - Error("Invalid root folder is found. Skip."); - continue; - } - - var config = Path.Combine(rootFolder, "php.ini"); - if (File.Exists(config)) - { - Info($"Found PHP config file {config}."); - var parser = new ConcatenateDuplicatedKeysIniDataParser(); - parser.Configuration.ConcatenateSeparator = " "; - var data = parser.Parse(File.ReadAllText(config)); - var extensionFolder = data["PHP"]["extension_dir"]; - if (extensionFolder == null) - { - extensionFolder = "\"ext\""; - } - - extensionFolder = extensionFolder.Trim('"'); - - var fullPath = Path.Combine(rootFolder, extensionFolder); - Info($"PHP loadable extension folder: {fullPath}"); - var extesionNames = data["PHP"]["extension"]; - if (extesionNames == null) - { - Info("No extension to verify."); - } - else - { - var extensions = extesionNames.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - Info($"Found {extensions.Length} extension(s) to verify."); - var noError = true; - foreach (var name in extensions) - { - var fileName = Path.Combine(fullPath, $"php_{name}.dll"); - if (!File.Exists(fileName)) - { - Error($"* Extension {name} is listed, but on disk the file cannot be found {fileName}"); - noError = false; - } - } - - if (noError) - { - Info("All extension(s) listed can be found on disk."); - } - } - } - else - { - Warn($"Cannot find PHP config file {config}. Default settings are used."); - } - - var matched = false; - foreach (var system in paths) - { - if (string.Equals(rootFolder, system, StringComparison.OrdinalIgnoreCase)) - { - matched = true; - break; - } - } - - if (matched) - { - Debug($"PHP installation has been added to Windows Path environment variable."); - } - else - { - Error($"PHP installation is not yet added to Windows Path environment variable. Please refer to https://learn.microsoft.com/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/hh994590(v=ws.11) for more details."); - Warn($"Restart Jexus Manager and rerun PHP Diagnostics after changing Windows Path environment variable."); - } - - Debug(string.Empty); - - // TODO: verify other configuration in php.info. - } - } - } - catch (Exception ex) - { - Debug(ex.ToString()); - System.Diagnostics.Debug.WriteLine(ex); - } + AnalyzePhpConfig(server, (Module)provider, knownPhpVersions); })); container.Add( @@ -360,5 +138,236 @@ private void PhpDiagDialogHelpButtonClicked(object sender, System.ComponentModel { BtnHelpClick(null, EventArgs.Empty); } + + private void AnalyzePhpConfig(ServerManager server, Module module, Dictionary knownPhpVersions) + { + try + { + Warn("IMPORTANT: This report might contain confidential information. Mask such before sharing with others."); + Warn("-----"); + Debug($"System Time: {DateTime.Now}"); + Debug($"Processor Architecture: {Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")}"); + Debug($"OS: {Environment.OSVersion}"); + Debug($"Server Type: {server.Mode.AsString(EnumFormat.Description)}"); + Debug(string.Empty); + + var modules = new ModulesFeature(module); + modules.Load(); + Debug($"Scan {modules.Items.Count} installed module(s)."); + if (modules.Items.All(item => item.Name != "FastCgiModule")) + { + Error($"FastCGI module is not installed as part of IIS. Please refer to https://learn.microsoft.com/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/hh994590(v=ws.11) for more details."); + return; + } + else + { + Debug("FastCGI module is installed."); + } + + Debug(string.Empty); + var handlers = new HandlersFeature(module); + handlers.Load(); + var foundPhpHandler = new List(); + Debug($"Scan {handlers.Items.Count} registered handler(s)."); + foreach (var item in handlers.Items) + { + if (item.Modules == "FastCgiModule") + { + if (File.Exists(item.ScriptProcessor)) + { + Debug($"* Found a valid FastCGI handler as {{ Name: {item.Name}, Path: {item.Path}, State: {item.GetState(handlers.AccessPolicy)}, Module: {item.TypeString}, Entry Type: {item.Flag} }}. File path: {item.ScriptProcessor}."); + foundPhpHandler.Add(item); + } + else + { + Error($"* Found an invalid FastCGI handler as {{Name: {item.Name}, Path: {item.Path}, State: {item.GetState(handlers.AccessPolicy)}, Module: {item.TypeString}, Entry Type: {item.Flag} }} because file path does not exist: {item.ScriptProcessor}."); + } + } + } + + if (foundPhpHandler.Count == 0) + { + Error($"No valid FastCGI handler is registered for this web site."); + Error($"To run PHP on IIS, please refer to https://learn.microsoft.com/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/hh994590(v=ws.11) for more details."); + return; + } + + Debug(string.Empty); + var fastCgiFeature = new FastCgiFeature(module); + fastCgiFeature.Load(); + Debug($"Scan {fastCgiFeature.Items.Count} registered FastCGI application(s)."); + var foundPhp = new List(); + foreach (var item in fastCgiFeature.Items) + { + var combination = string.IsNullOrWhiteSpace(item.Arguments) ? item.Path : item.Path + '|' + item.Arguments; + foreach (var handler in foundPhpHandler) + { + if (string.Equals(combination, handler.ScriptProcessor, StringComparison.OrdinalIgnoreCase)) + { + Debug($"* Found FastCGI application registered as {{ Full path: {item.Path}, Arguments: {item.Arguments} }}."); + foundPhp.Add(item); + break; + } + } + } + + if (foundPhp.Count == 0) + { + Error($"No suitable FastCGI appilcation is registered on this server."); + Error($"To run PHP on IIS, please refer to https://learn.microsoft.com/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/hh994590(v=ws.11) for more details."); + return; + } + + Debug(Environment.NewLine); + Debug($"Verify web stack installation versions."); + foreach (var item in foundPhp) + { + var path = item.Path; + if (path.TrimEnd('"').EndsWith("php-cgi.exe", StringComparison.OrdinalIgnoreCase)) + { + // PHP + var info = FileVersionInfo.GetVersionInfo(path); + var version = $"{info.FileMajorPart}.{info.FileMinorPart}"; + if (knownPhpVersions.ContainsKey(version)) + { + var matched = knownPhpVersions[version]; + if (matched.ExpiringDate <= DateTime.Now) + { + Error($"* PHP {info.FileVersion} ({path}) is unknown or obsolete. Please refer to http://php.net/supported-versions.php for more details."); + } + else if (matched.ExpiringDate > DateTime.Now && (matched.ExpiringDate - DateTime.Now).TotalDays < 180) + { + Warn($"* PHP {version} ({path}) will soon be obsolete. Please refer to http://php.net/supported-versions.php for more details."); + } + else + { + Debug($"* PHP {version} ({path}) is supported."); + } + + var x86 = DialogHelper.GetImageArchitecture(path); + var cppFile = Path.Combine( + Environment.GetFolderPath(x86 ? Environment.SpecialFolder.SystemX86 : Environment.SpecialFolder.System), + $"msvcp{matched.CppVersion.Major}0.dll"); + if (File.Exists(cppFile)) + { + var cpp = FileVersionInfo.GetVersionInfo(cppFile); + if (cpp.FileMinorPart >= matched.CppVersion.Minor) + { + Debug($" Visual C++ runtime is detected (expected: {matched.CppVersion}, detected: {cpp.FileVersion}): {cppFile}."); + } + else + { + Error($" Visual C++ runtime {matched.CppVersion} is not detected. Please install it following the tips on https://windows.php.net/download/."); + } + } + else + { + Error($" Visual C++ {matched.CppVersion} runtime is not detected. Please install it following the tips on https://windows.php.net/download/."); + } + } + else + { + Error($"* PHP {info.FileVersion} ({path}) is unknown or obsolete. Please refer to http://php.net/supported-versions.php for more details."); + } + } + } + + Debug(string.Empty); + var systemPath = Environment.GetEnvironmentVariable("Path"); + Debug($"Windows Path environment variable: {systemPath}."); + Debug(string.Empty); + string[] paths = systemPath.Split(new char[1] { Path.PathSeparator }); + foreach (var item in foundPhp) + { + var path = item.Path; + if (path.TrimEnd('"').EndsWith("php-cgi.exe", StringComparison.OrdinalIgnoreCase)) + { + var rootFolder = Path.GetDirectoryName(path); + Debug($"[{rootFolder}]"); + if (!Directory.Exists(rootFolder)) + { + Error("Invalid root folder is found. Skip."); + continue; + } + + var config = Path.Combine(rootFolder, "php.ini"); + if (File.Exists(config)) + { + Info($"Found PHP config file {config}."); + var parser = new ConcatenateDuplicatedKeysIniDataParser(); + parser.Configuration.ConcatenateSeparator = " "; + var data = parser.Parse(File.ReadAllText(config)); + var extensionFolder = data["PHP"]["extension_dir"]; + if (extensionFolder == null) + { + extensionFolder = "\"ext\""; + } + + extensionFolder = extensionFolder.Trim('"'); + + var fullPath = Path.Combine(rootFolder, extensionFolder); + Info($"PHP loadable extension folder: {fullPath}"); + var extesionNames = data["PHP"]["extension"]; + if (extesionNames == null) + { + Info("No extension to verify."); + } + else + { + var extensions = extesionNames.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + Info($"Found {extensions.Length} extension(s) to verify."); + var noError = true; + foreach (var name in extensions) + { + var fileName = Path.Combine(fullPath, $"php_{name}.dll"); + if (!File.Exists(fileName)) + { + Error($"* Extension {name} is listed, but on disk the file cannot be found {fileName}"); + noError = false; + } + } + + if (noError) + { + Info("All extension(s) listed can be found on disk."); + } + } + } + else + { + Warn($"Cannot find PHP config file {config}. Default settings are used."); + } + + var matched = false; + foreach (var system in paths) + { + if (string.Equals(rootFolder, system, StringComparison.OrdinalIgnoreCase)) + { + matched = true; + break; + } + } + + if (matched) + { + Debug($"PHP installation has been added to Windows Path environment variable."); + } + else + { + Error($"PHP installation is not yet added to Windows Path environment variable. Please refer to https://learn.microsoft.com/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/hh994590(v=ws.11) for more details."); + Warn($"Restart Jexus Manager and rerun PHP Diagnostics after changing Windows Path environment variable."); + } + + Debug(string.Empty); + + // TODO: verify other configuration in php.info. + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error analyzing PHP configuration"); + } + } } } diff --git a/JexusManager/Features/Main/SslDiagDialog.cs b/JexusManager/Features/Main/SslDiagDialog.cs index 731b22f1..9d8444c4 100644 --- a/JexusManager/Features/Main/SslDiagDialog.cs +++ b/JexusManager/Features/Main/SslDiagDialog.cs @@ -21,9 +21,12 @@ namespace JexusManager.Features.Main using System.Reactive.Linq; using System.Collections.Generic; using EnumsNET; + using Microsoft.Extensions.Logging; public partial class SslDiagDialog : DialogForm { + private static readonly ILogger _logger = LogHelper.GetLogger("SslDiagDialog"); + public SslDiagDialog(IServiceProvider provider, ServerManager server) : base(provider) { @@ -204,7 +207,7 @@ public SslDiagDialog(IServiceProvider provider, ServerManager server) else { Error("#You have a private key that corresponds to this certificate but CryptAcquireCertificatePrivateKey failed."); - System.Diagnostics.Debug.WriteLine("CryptAcquireCertificatePrivateKey failed"); + _logger.LogWarning("CryptAcquireCertificatePrivateKey failed"); } if (shouldRelease) @@ -349,7 +352,7 @@ public SslDiagDialog(IServiceProvider provider, ServerManager server) Error($"Problems detected on certificate store {binding.CertificateStoreName}."); if (ex.HResult != Microsoft.Web.Administration.NativeMethods.NonExistingStore) { - System.Diagnostics.Debug.WriteLine($"CryptographicException {ex.HResult} from SslDiag."); + _logger.LogError(ex, "CryptographicException {HResult} from SslDiag", ex.HResult); throw; } @@ -364,12 +367,12 @@ public SslDiagDialog(IServiceProvider provider, ServerManager server) catch (CryptographicException ex) { Debug(ex.ToString()); - System.Diagnostics.Debug.WriteLine(ex); - System.Diagnostics.Debug.WriteLine($"hResult {ex.HResult}"); + _logger.LogError(ex, "CryptographicException {HResult} from SslDiag", ex.HResult); } catch (Exception ex) { Debug(ex.ToString()); + _logger.LogError(ex, "Error analyzing certificate. HResult: {HResult}", ex.HResult); } })); diff --git a/JexusManager/Features/Main/VsDiagDialog.cs b/JexusManager/Features/Main/VsDiagDialog.cs index 0a05d956..9d488276 100644 --- a/JexusManager/Features/Main/VsDiagDialog.cs +++ b/JexusManager/Features/Main/VsDiagDialog.cs @@ -19,9 +19,14 @@ namespace JexusManager.Features.Main using System.Xml.XPath; using Newtonsoft.Json.Linq; using System.Linq; + using System.Windows.Forms; + using Microsoft.Extensions.Logging; + using JexusManager; public partial class VsDiagDialog : DialogForm { + private static readonly ILogger _logger = LogHelper.GetLogger("VsDiagDialog"); + public VsDiagDialog(IServiceProvider provider, Site site) : base(provider) { @@ -102,7 +107,7 @@ public VsDiagDialog(IServiceProvider provider, Site site) catch (Exception ex) { Debug(ex.ToString()); - System.Diagnostics.Debug.WriteLine(ex); + _logger.LogError(ex, "Error analyzing VS configuration"); } })); @@ -191,7 +196,7 @@ private void AnalyzeAspNetCoreProject(string project, Site site) Info($"applicationUrl is {rawUrl}."); var iisUrl = sslPort == 0 ? rawUrl : $"https://localhost:{sslPort}/"; var matched = false; - foreach (Binding binding in site.Bindings) + foreach (Microsoft.Web.Administration.Binding binding in site.Bindings) { Info($"Binding {binding.ToShortString()}."); if (string.Equals(binding.ToIisUrl(), iisUrl, StringComparison.OrdinalIgnoreCase)) @@ -229,92 +234,98 @@ private static bool IsAspNetCoreProject(XDocument xml) private void AnalyzeWebProject(XDocument xml, XmlNamespaceManager namespaceManager, XNamespace name, Site site) { - Debug($"Analyze ASP.NET project."); - Debug($"Extract web project settings."); - var webSettings = xml.Root.XPathSelectElement("/x:Project/x:ProjectExtensions/x:VisualStudio/x:FlavorProperties/x:WebProjectProperties", namespaceManager); - var useIIS = webSettings.Element(name + "UseIIS")?.Value; - Info($"UseIIS: {useIIS}"); - var autoAssignPort = webSettings.Element(name + "AutoAssignPort")?.Value; - Info($"AutoAssignPort: {autoAssignPort}"); - var developmentServerPort = webSettings.Element(name + "DevelopmentServerPort")?.Value; - Info($"DevelopmentServerPort: {developmentServerPort}"); - var developmentServerVPath = webSettings.Element(name + "DevelopmentServerVPath")?.Value; - Info($"DevelopmentServerVPath: {developmentServerVPath}"); - var iisUrl = webSettings.Element(name + "IISUrl")?.Value; - Info($"IISUrl: {iisUrl}"); - var ntlmAuthentication = webSettings.Element(name + "NTLMAuthentication")?.Value; - Info($"NTLMAuthentication: {ntlmAuthentication}"); - var useCustomServer = webSettings.Element(name + "UseCustomServer")?.Value; - Info($"UseCustomServer: {useCustomServer}"); - var customServerUrl = webSettings.Element(name + "CustomServerUrl")?.Value; - Info($"CustomServerUrl: {customServerUrl}"); - var saveServerSettingsInUserFile = webSettings.Element(name + "SaveServerSettingsInUserFile")?.Value; - Info($"SaveServerSettingsInUserFile: {saveServerSettingsInUserFile}"); - - var useIISExpress = xml.Root.XPathSelectElement("/x:Project/x:PropertyGroup/x:UseIISExpress", namespaceManager)?.Value; - Info($"UseIISExpress: {useIISExpress}"); - var iisExpressSSLPort = xml.Root.XPathSelectElement("/x:Project/x:PropertyGroup/x:IISExpressSSLPort", namespaceManager)?.Value; - Info($"IISExpressSSLPort: {iisExpressSSLPort}"); - var iisExpressAnonymousAuthentication = xml.Root.XPathSelectElement("/x:Project/x:PropertyGroup/x:IISExpressAnonymousAuthentication", namespaceManager)?.Value; - Info($"IISExpressAnonymousAuthentication: {iisExpressAnonymousAuthentication}"); - var iisExpressWindowsAuthentication = xml.Root.XPathSelectElement("/x:Project/x:PropertyGroup/x:IISExpressWindowsAuthentication", namespaceManager)?.Value; - Info($"IISExpressWindowsAuthentication: {iisExpressWindowsAuthentication}"); - var iisExpressUseClassicPipelineMode = xml.Root.XPathSelectElement("/x:Project/x:PropertyGroup/x:IISExpressUseClassicPipelineMode", namespaceManager)?.Value; - Info($"IISExpressUseClassicPipelineMode: {iisExpressUseClassicPipelineMode}"); - var useGlobalApplicationHostFile = xml.Root.XPathSelectElement("/x:Project/x:PropertyGroup/x:UseGlobalApplicationHostFile", namespaceManager)?.Value; - Info($"UseGlobalApplicationHostFile: {useGlobalApplicationHostFile}"); - - Info($"Scan all bindings."); - bool matched = false; - if (string.Equals(useIIS, "true", StringComparison.OrdinalIgnoreCase)) + try { - if (string.Equals(useIISExpress, "true", StringComparison.OrdinalIgnoreCase)) + Debug($"Analyze ASP.NET project."); + Debug($"Extract web project settings."); + var webSettings = xml.Root.XPathSelectElement("/x:Project/x:ProjectExtensions/x:VisualStudio/x:FlavorProperties/x:WebProjectProperties", namespaceManager); + var useIIS = webSettings.Element(name + "UseIIS")?.Value; + Info($"UseIIS: {useIIS}"); + var autoAssignPort = webSettings.Element(name + "AutoAssignPort")?.Value; + Info($"AutoAssignPort: {autoAssignPort}"); + var developmentServerPort = webSettings.Element(name + "DevelopmentServerPort")?.Value; + Info($"DevelopmentServerPort: {developmentServerPort}"); + var developmentServerVPath = webSettings.Element(name + "DevelopmentServerVPath")?.Value; + Info($"DevelopmentServerVPath: {developmentServerVPath}"); + var iisUrl = webSettings.Element(name + "IISUrl")?.Value; + Info($"IISUrl: {iisUrl}"); + var ntlmAuthentication = webSettings.Element(name + "NTLMAuthentication")?.Value; + Info($"NTLMAuthentication: {ntlmAuthentication}"); + var useCustomServer = webSettings.Element(name + "UseCustomServer")?.Value; + Info($"UseCustomServer: {useCustomServer}"); + var customServerUrl = webSettings.Element(name + "CustomServerUrl")?.Value; + Info($"CustomServerUrl: {customServerUrl}"); + var saveServerSettingsInUserFile = webSettings.Element(name + "SaveServerSettingsInUserFile")?.Value; + Info($"SaveServerSettingsInUserFile: {saveServerSettingsInUserFile}"); + + var useIISExpress = xml.Root.XPathSelectElement("/x:Project/x:PropertyGroup/x:UseIISExpress", namespaceManager)?.Value; + Info($"UseIISExpress: {useIISExpress}"); + var iisExpressSSLPort = xml.Root.XPathSelectElement("/x:Project/x:PropertyGroup/x:IISExpressSSLPort", namespaceManager)?.Value; + Info($"IISExpressSSLPort: {iisExpressSSLPort}"); + var iisExpressAnonymousAuthentication = xml.Root.XPathSelectElement("/x:Project/x:PropertyGroup/x:IISExpressAnonymousAuthentication", namespaceManager)?.Value; + Info($"IISExpressAnonymousAuthentication: {iisExpressAnonymousAuthentication}"); + var iisExpressWindowsAuthentication = xml.Root.XPathSelectElement("/x:Project/x:PropertyGroup/x:IISExpressWindowsAuthentication", namespaceManager)?.Value; + Info($"IISExpressWindowsAuthentication: {iisExpressWindowsAuthentication}"); + var iisExpressUseClassicPipelineMode = xml.Root.XPathSelectElement("/x:Project/x:PropertyGroup/x:IISExpressUseClassicPipelineMode", namespaceManager)?.Value; + Info($"IISExpressUseClassicPipelineMode: {iisExpressUseClassicPipelineMode}"); + var useGlobalApplicationHostFile = xml.Root.XPathSelectElement("/x:Project/x:PropertyGroup/x:UseGlobalApplicationHostFile", namespaceManager)?.Value; + Info($"UseGlobalApplicationHostFile: {useGlobalApplicationHostFile}"); + + Info($"Scan all bindings."); + bool matched = false; + if (string.Equals(useIIS, "true", StringComparison.OrdinalIgnoreCase)) { - // IIS Express - Info($"IIS Express is used for this project."); - foreach (Binding binding in site.Bindings) + if (string.Equals(useIISExpress, "true", StringComparison.OrdinalIgnoreCase)) { - Info($"Binding {binding.ToShortString()}."); - if (string.Equals(binding.ToIisUrl(), iisUrl, StringComparison.OrdinalIgnoreCase)) + // IIS Express + Info($"IIS Express is used for this project."); + foreach (Microsoft.Web.Administration.Binding binding in site.Bindings) { - Info($"A matching binding is found for {iisUrl}."); - matched = true; + Info($"Binding {binding.ToShortString()}."); + if (string.Equals(binding.ToIisUrl(), iisUrl, StringComparison.OrdinalIgnoreCase)) + { + Info($"A matching binding is found for {iisUrl}."); + matched = true; + } } } - } - else if (string.Equals(useIISExpress, "false", StringComparison.OrdinalIgnoreCase)) - { - // IIS - Info($"Full IIS is used for this project."); - foreach (Binding binding in site.Bindings) + else if (string.Equals(useIISExpress, "false", StringComparison.OrdinalIgnoreCase)) { - Info($"Binding {binding.ToShortString()}."); - if (string.Equals(binding.ToIisUrl(), iisUrl, StringComparison.OrdinalIgnoreCase)) + // IIS + Info($"Full IIS is used for this project."); + foreach (Microsoft.Web.Administration.Binding binding in site.Bindings) { - Info($"A matching binding is found for {iisUrl}."); - matched = true; + Info($"Binding {binding.ToShortString()}."); + if (string.Equals(binding.ToIisUrl(), iisUrl, StringComparison.OrdinalIgnoreCase)) + { + Info($"A matching binding is found for {iisUrl}."); + matched = true; + } } } + else + { + Warn($"An unexpected condition hit."); + _logger.LogWarning("Unexpected condition hit"); + return; + } } else { - //TODO: What? - Warn($"An unexpected condition hit."); - System.Diagnostics.Debug.WriteLine("unexpected condition hit."); + // External server + Warn($"External server is used."); return; } - } - else - { - // External server - Warn($"External server is used."); - return; - } - if (!matched) + if (!matched) + { + Error($"No matching binding is found for {iisUrl}."); + Error($"Please edit project file and IIS Express configuration file to match each other."); + } + } + catch (Exception ex) { - Error($"No matching binding is found for {iisUrl}."); - Error($"Please edit project file and IIS Express configuration file to match each other."); + _logger.LogError(ex, "Error analyzing web project"); } } diff --git a/JexusManager/JexusManager.csproj b/JexusManager/JexusManager.csproj index 9c54a135..7926dd98 100644 --- a/JexusManager/JexusManager.csproj +++ b/JexusManager/JexusManager.csproj @@ -50,6 +50,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/JexusManager/LoggingService.cs b/JexusManager/LoggingService.cs new file mode 100644 index 00000000..c9e9458b --- /dev/null +++ b/JexusManager/LoggingService.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Concurrent; +using System.Drawing; +using System.Windows.Forms; +using Microsoft.Extensions.Logging; + +namespace JexusManager +{ + public class TextBoxLoggerProvider : ILoggerProvider + { + private readonly RichTextBox _textBox; + private readonly ConcurrentQueue<(string text, Color color)> _pendingMessages = new ConcurrentQueue<(string, Color)>(); + private readonly object _lock = new object(); + private bool _isInitialized; + private Timer _initCheckTimer; + + public TextBoxLoggerProvider(RichTextBox textBox) + { + _textBox = textBox; + + // Start a timer to check for control initialization + _initCheckTimer = new Timer(); + _initCheckTimer.Interval = 100; // Check every 100ms + _initCheckTimer.Tick += (s, e) => { + if (_textBox.IsHandleCreated && !_isInitialized) + { + lock (_lock) + { + if (!_isInitialized) + { + _isInitialized = true; + // Process any pending messages + while (_pendingMessages.TryDequeue(out var message)) + { + AppendTextSafe(message.text, message.color); + } + _initCheckTimer.Stop(); + _initCheckTimer.Dispose(); + _initCheckTimer = null; + } + } + } + }; + _initCheckTimer.Start(); + } + + public ILogger CreateLogger(string categoryName) + { + return new TextBoxLogger(this, categoryName); + } + + public void Dispose() + { + _initCheckTimer?.Dispose(); + } + + internal void LogMessage(string message, Color color) + { + if (!_isInitialized) + { + _pendingMessages.Enqueue((message + Environment.NewLine, color)); + return; + } + + AppendTextSafe(message + Environment.NewLine, color); + } + + private void AppendTextSafe(string text, Color color) + { + if (_textBox.IsDisposed) + return; + + if (_textBox.InvokeRequired) + { + try + { + _textBox.Invoke(new Action(() => AppendTextSafe(text, color))); + } + catch (ObjectDisposedException) + { + // Form might be closing + } + return; + } + + int start = _textBox.TextLength; + _textBox.AppendText(text); + int end = _textBox.TextLength; + + // Color the new text + _textBox.Select(start, end - start); + _textBox.SelectionColor = color; + _textBox.SelectionLength = 0; // Clear selection + + // Keep last 1000 lines + while (_textBox.Lines.Length > 1000) + { + var index = _textBox.GetFirstCharIndexFromLine(0); + var length = _textBox.GetFirstCharIndexFromLine(1) - index; + if (length > 0) + _textBox.Text = _textBox.Text.Remove(index, length); + } + + _textBox.SelectionStart = _textBox.TextLength; + _textBox.ScrollToCaret(); + } + } + + public class TextBoxLogger : ILogger + { + private readonly TextBoxLoggerProvider _provider; + + public TextBoxLogger(TextBoxLoggerProvider provider, string categoryName) + { + _provider = provider; + } + + public IDisposable BeginScope(TState state) + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + return; + + // Convert log levels to short names and get appropriate colors + var (levelText, color) = logLevel switch + { + LogLevel.Trace => ("trc", Color.Gray), + LogLevel.Debug => ("dbg", Color.LightGray), + LogLevel.Information => ("inf", Color.White), + LogLevel.Warning => ("wrn", Color.Yellow), + LogLevel.Error => ("err", Color.Red), + LogLevel.Critical => ("crt", Color.Magenta), + _ => ("unk", Color.DarkGray) + }; + + var message = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{levelText}] {formatter(state, exception)}"; + if (exception != null) + message += Environment.NewLine + exception; + + _provider.LogMessage(message, color); + } + } +} \ No newline at end of file diff --git a/JexusManager/MainForm.cs b/JexusManager/MainForm.cs index d2a1f56f..5340f05b 100644 --- a/JexusManager/MainForm.cs +++ b/JexusManager/MainForm.cs @@ -58,9 +58,13 @@ namespace JexusManager using JexusManager.Features.Asp; using JexusManager.Features.TraceFailedRequests; using System.Diagnostics; + using Microsoft.Extensions.Logging; + using System.Drawing; public sealed partial class MainForm : Form { + private readonly Panel _logPanel; + private readonly RichTextBox _logTextBox; private const string expressGlobalInstanceName = "Global"; private readonly List _providers; private readonly ServiceContainer _serviceContainer; @@ -68,13 +72,39 @@ public sealed partial class MainForm : Form private TreeNode IisExpressRoot { get; } private TreeNode IisRoot { get; } private TreeNode JexusRoot { get; } + private TreeNode StartPage { get; set; } public ManagementUIService UIService { get; } - public MainForm(List files) + public MainForm(List files, RichTextBox texBox) { InitializeComponent(); + // Create logging panel + _logTextBox = texBox; + + _logPanel = new Panel + { + Dock = DockStyle.Bottom, + Height = 200, + Visible = false + }; + _logPanel.Controls.Add(_logTextBox); + Controls.Add(_logPanel); + + LogHelper.GetLogger().LogInformation("Jexus Manager starting up. Version: {Version}", + Assembly.GetExecutingAssembly().GetName().Version); + + // Add toggle logging menu item + helpToolStripMenuItem.DropDownItems.Add(new ToolStripSeparator()); + var toggleLogItem = new ToolStripMenuItem("Show &Logs", null, (s, e) => + { + _logPanel.Visible = !_logPanel.Visible; + ((ToolStripMenuItem)s).Text = _logPanel.Visible ? "Hide &Logs" : "Show &Logs"; + }); + helpToolStripMenuItem.DropDownItems.Add(toggleLogItem); + + removeToolStripMenuItem.Image = DefaultTaskList.RemoveImage; removeToolStripMenuItem1.Image = DefaultTaskList.RemoveImage; btnRemoveFarmServer.Image = DefaultTaskList.RemoveImage; @@ -97,7 +127,8 @@ public MainForm(List files) actRunAsAdmin.Image = NativeMethods.GetShieldIcon(); btnAbout.Text = string.Format("About Jexus Manager {0}", Assembly.GetExecutingAssembly().GetName().Version); - treeView1.Nodes.Add(new PlaceholderTreeNode("Start Page", 0) { ContextMenuStrip = cmsIis }); + StartPage = new PlaceholderTreeNode("Start Page", 0) { ContextMenuStrip = cmsIis }; + treeView1.Nodes.Add(StartPage); if (!Helper.IsRunningOnMono()) { IisExpressRoot = new PlaceholderTreeNode("IIS Express", 10) { ContextMenuStrip = cmsIis }; @@ -217,8 +248,10 @@ internal Action SaveMenuItem private void LoadIisExpress() { + LogHelper.GetLogger().LogInformation("Loading IIS Express servers"); if (!IisExpressServerManager.ServerInstalled) { + LogHelper.GetLogger().LogWarning("IIS Express is not installed on this machine"); return; } @@ -327,6 +360,7 @@ private void LoadIis() private void RegisterServer(ServerTreeNode data) { + LogHelper.GetLogger().LogInformation("Registering server {Name} ({Mode})", data.DisplayName, data.Mode); data.ContextMenuStrip = cmsServer; if (data.Mode == WorkingMode.IisExpress) { @@ -383,24 +417,28 @@ private void actCreateSite_Execute(object sender, EventArgs e) var selected = treeView1.SelectedNode; if (selected == null) { + LogHelper.GetLogger().LogWarning("Cannot create site: no node selected"); return; } var data = GetCurrentData(selected); if (data.IsBusy) { + LogHelper.GetLogger().LogWarning("Cannot create site: server {Name} is busy", data.DisplayName); return; } if (data.ServerManager == null) { - Debug.WriteLine($"null server: {data.DisplayName} : {data.Mode} : {selected.Text} : {selected.GetType().FullName}"); + LogHelper.GetLogger().LogWarning("Null server: {DisplayName} : {Mode} : {Text} : {Type}", + data.DisplayName, data.Mode, selected.Text, selected.GetType().FullName); return; } if (data.ServerManager.Sites == null) { - Debug.WriteLine($"null sites collection: {data.DisplayName} : {data.Mode} : {selected.Text} : {selected.GetType().FullName} : {data.ServerManager.FileName}"); + LogHelper.GetLogger().LogWarning("Null sites collection: {DisplayName} : {Mode} : {Text} : {Type} : {FileName}", + data.DisplayName, data.Mode, selected.Text, selected.GetType().FullName, data.ServerManager.FileName); return; } @@ -412,9 +450,11 @@ private void actCreateSite_Execute(object sender, EventArgs e) return; } + LogHelper.GetLogger().LogInformation("Creating new site on server {Server}", data.DisplayName); data.ServerManager.Sites.Add(dialog.NewSite); dialog.NewSite.Applications[0].Save(); data.ServerManager.CommitChanges(); + LogHelper.GetLogger().LogInformation("Site {SiteName} created successfully", dialog.NewSite.Name); AddSiteNode(dialog.NewSite); } @@ -492,6 +532,7 @@ private void btnRemoveSite_Click(object sender, EventArgs e) private void Form1FormClosing(object sender, FormClosingEventArgs e) { + LogHelper.GetLogger().LogInformation("Application shutting down"); if (actSave.Enabled) { var result = UIService.ShowMessage("The connection list has changed. Do you want to save changes?", Text, MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); @@ -515,7 +556,7 @@ private void Form1FormClosing(object sender, FormClosingEventArgs e) } catch (Exception ex) { - Debug.WriteLine(ex); + LogHelper.GetLogger().LogError(ex, "Error during server shutdown"); var last = ex; Exception previous = null; while (last.InnerException != null) @@ -664,7 +705,7 @@ private void treeView1_AfterSelect(object sender, TreeViewEventArgs e) { if (e.Node.Text != ManagerTreeNode.TempNodeName) { - Debug.WriteLine($"wrong node {e.Node.GetType().FullName} {e.Node.Text}"); + LogHelper.GetLogger().LogWarning("Wrong node type: {Type} Name: {Text}", e.Node.GetType().FullName, e.Node.Text); } return; @@ -939,7 +980,7 @@ internal void ConnectToServer() } catch (Exception ex) { - Debug.WriteLine(ex); + LogHelper.GetLogger().LogError(ex, "Error during server shutdown"); File.WriteAllText(DialogHelper.DebugLog, ex.ToString()); var last = ex; while (last is AggregateException) @@ -1100,7 +1141,8 @@ private void treeView1_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEve private async void btnUpdate_Click(object sender, EventArgs e) { - await UpdateHelper.FindUpdate(); + // Navigate to the Start Page which will display update information + treeView1.SelectedNode = StartPage; } private void btnRemoveFarmServer_Click(object sender, EventArgs e) diff --git a/JexusManager/Program.cs b/JexusManager/Program.cs index a3c873a8..3bef1b13 100644 --- a/JexusManager/Program.cs +++ b/JexusManager/Program.cs @@ -9,15 +9,20 @@ namespace JexusManager { using JexusManager.Dialogs; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; using Mono.Options; using System; using System.Collections.Generic; + using System.Drawing; using System.IO; using System.Runtime.InteropServices; using System.Windows.Forms; internal static class Program { + public static IServiceProvider ServiceProvider { get; private set; } + /// /// The main entry point for the application. /// @@ -50,12 +55,32 @@ private static void Main(string[] args) Microsoft.Web.Administration.JexusServerManager.Enabled = jexus; + // UI initialization ApplicationConfiguration.Initialize(); - - // TODO: set encryption support - // ProtectedConfigurationProvider.Provider = new WorkingEncryptionServiceProvider(); + var textBox = new RichTextBox + { + Multiline = true, + ReadOnly = true, + Dock = DockStyle.Fill, + ScrollBars = RichTextBoxScrollBars.Vertical, + Font = new Font("Consolas", 9), + BackColor = Color.Black, + ForeColor = Color.LightGray + }; + + var provider = new TextBoxLoggerProvider(textBox); + var services = new ServiceCollection(); + services.AddLogging(builder => + { + builder.AddProvider(provider); + }); + + ServiceProvider = services.BuildServiceProvider(); + + var loggerFactory = ServiceProvider.GetRequiredService(); + LogHelper.Initialize(loggerFactory); - Application.Run(new MainForm(extra)); + Application.Run(new MainForm(extra, textBox)); } private static void ShowHelp(OptionSet optionSet) diff --git a/JexusManager/Tree/ManagerTreeNode.cs b/JexusManager/Tree/ManagerTreeNode.cs index e23437b0..98c31d75 100644 --- a/JexusManager/Tree/ManagerTreeNode.cs +++ b/JexusManager/Tree/ManagerTreeNode.cs @@ -5,18 +5,22 @@ namespace JexusManager.Tree { using System; - using System.Collections.Generic; - using System.ComponentModel.Design; using System.IO; - using System.Windows.Forms; + using Microsoft.Extensions.Logging; + using JexusManager; using Microsoft.Web.Administration; using Microsoft.Web.Management.Server; using Application = Microsoft.Web.Administration.Application; + using System.Collections.Generic; + using System.Windows.Forms; + using System.ComponentModel.Design; internal abstract class ManagerTreeNode : TreeNode { + private static readonly ILogger _logger = LogHelper.GetLogger("ManagerTreeNode"); + public const string TempNodeName = "ManagerTreeNodeTemp"; public IServiceProvider ServiceProvider { get; set; } @@ -41,29 +45,29 @@ protected ManagerTreeNode(string text, IServiceProvider serviceProvider) : base( protected void LoadChildren(Application rootApp, int rootLevel, string rootFolder, string pathToSite, ContextMenuStrip phyMenu, ContextMenuStrip vDirMenu, ContextMenuStrip appMenu) { - var treeNodes = new List(); - foreach (VirtualDirectory virtualDirectory in rootApp.VirtualDirectories) + try { - var path = virtualDirectory.PathToSite(); - if (!path.StartsWith(pathToSite)) + var treeNodes = new List(); + foreach (VirtualDirectory virtualDirectory in rootApp.VirtualDirectories) { - continue; - } + var path = virtualDirectory.PathToSite(); + if (!path.StartsWith(pathToSite)) + { + continue; + } - if (GetLevel(path) != rootLevel + 1) - { - continue; - } + if (GetLevel(path) != rootLevel + 1) + { + continue; + } - // IMPORTANT: only create level+1 vDir nodes. - var virtualDirectoryNode = new VirtualDirectoryTreeNode(ServiceProvider, virtualDirectory, ServerNode) { ContextMenuStrip = vDirMenu }; - treeNodes.Add(virtualDirectoryNode); - } + // IMPORTANT: only create level+1 vDir nodes. + var virtualDirectoryNode = new VirtualDirectoryTreeNode(ServiceProvider, virtualDirectory, ServerNode) { ContextMenuStrip = vDirMenu }; + treeNodes.Add(virtualDirectoryNode); + } - var loaded = new HashSet(); - if (Directory.Exists(rootFolder)) - { - try + var loaded = new HashSet(); + if (Directory.Exists(rootFolder)) { // IMPORTANT: only create level+1 physical nodes. foreach (var folder in new DirectoryInfo(rootFolder).GetDirectories()) @@ -105,46 +109,46 @@ protected void LoadChildren(Application rootApp, int rootLevel, string rootFolde treeNodes.Add(directory); } } - catch (IOException ex) - { - System.Diagnostics.Debug.WriteLine(ex); - } - } - foreach (Application application in rootApp.Site.Applications) - { - if (application.IsRoot()) + foreach (Application application in rootApp.Site.Applications) { - continue; - } + if (application.IsRoot()) + { + continue; + } - if (!application.Path.StartsWith(pathToSite)) - { - continue; - } + if (!application.Path.StartsWith(pathToSite)) + { + continue; + } - if (loaded.Contains(application.Path)) - { - continue; - } + if (loaded.Contains(application.Path)) + { + continue; + } - if (GetLevel(application.Path) != rootLevel + 1) - { - continue; - } + if (GetLevel(application.Path) != rootLevel + 1) + { + continue; + } - if (application.VirtualDirectories.Count == 0) - { - continue; + if (application.VirtualDirectories.Count == 0) + { + continue; + } + + // IMPORTANT: only create level+1 physical nodes. + var appNode = new ApplicationTreeNode(ServiceProvider, application, ServerNode) { ContextMenuStrip = appMenu }; + treeNodes.Add(appNode); } - // IMPORTANT: only create level+1 physical nodes. - var appNode = new ApplicationTreeNode(ServiceProvider, application, ServerNode) { ContextMenuStrip = appMenu }; - treeNodes.Add(appNode); + treeNodes.Sort(s_comparer); + Nodes.AddRange(treeNodes.ToArray()); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error loading children"); } - - treeNodes.Sort(s_comparer); - Nodes.AddRange(treeNodes.ToArray()); } private static readonly TreeNodeComparer s_comparer = new TreeNodeComparer(); @@ -168,7 +172,7 @@ public void Explore() public void EditPermissions() { - NativeMethods.ShowFileProperties(Folder); + Microsoft.Web.Administration.NativeMethods.ShowFileProperties(Folder); } public void Browse() diff --git a/JexusManager/Tree/PlaceholderTreeNode.cs b/JexusManager/Tree/PlaceholderTreeNode.cs index 6b3439b6..3b68d9ca 100644 --- a/JexusManager/Tree/PlaceholderTreeNode.cs +++ b/JexusManager/Tree/PlaceholderTreeNode.cs @@ -17,6 +17,8 @@ namespace JexusManager.Tree internal sealed class PlaceholderTreeNode : ManagerTreeNode { + private static HomePage _homePage; + public PlaceholderTreeNode(string name, int image) : base(name, null) { @@ -36,7 +38,12 @@ public PlaceholderTreeNode(string name, int image) public override void LoadPanels(MainForm mainForm, ServiceContainer serviceContainer, List moduleProviders) { - mainForm.LoadInner(new HomePage()); + if (_homePage == null) + { + _homePage = new HomePage(); + } + + mainForm.LoadInner(_homePage); } public override void HandleDoubleClick() diff --git a/JexusManager/Tree/ServerTreeNode.cs b/JexusManager/Tree/ServerTreeNode.cs index 85d9fab7..3bd33abd 100644 --- a/JexusManager/Tree/ServerTreeNode.cs +++ b/JexusManager/Tree/ServerTreeNode.cs @@ -3,24 +3,26 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Collections.Generic; -using System.ComponentModel.Design; -using System.IO; -using System.Text; using System.Windows.Forms; - +using Microsoft.Extensions.Logging; +using JexusManager; +using Microsoft.Web.Administration; +using System.IO; using JexusManager.Dialogs; -using JexusManager.Features.Main; +using System.ComponentModel.Design; +using System.Text; using JexusManager.Services; - -using Microsoft.Web.Administration; -using Microsoft.Web.Management.Client; +using System.Collections.Generic; using Microsoft.Web.Management.Server; +using JexusManager.Features.Main; +using Microsoft.Web.Management.Client; namespace JexusManager.Tree { - internal sealed class ServerTreeNode : ManagerTreeNode + internal class ServerTreeNode : ManagerTreeNode { + private static readonly ILogger _logger = LogHelper.GetLogger("ServerTreeNode"); + private enum NodeStatus { Default = 0, @@ -233,7 +235,7 @@ public override void HandleDoubleClick() } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine(ex); + _logger.LogError(ex, "Error connecting to server"); File.WriteAllText(DialogHelper.DebugLog, ex.ToString()); var last = ex; while (last is AggregateException) diff --git a/JexusManager/UpdateHelper.cs b/JexusManager/UpdateHelper.cs index 47870520..8a4a5103 100644 --- a/JexusManager/UpdateHelper.cs +++ b/JexusManager/UpdateHelper.cs @@ -13,8 +13,23 @@ namespace JexusManager internal static class UpdateHelper { - public static async Task FindUpdate() + public class UpdateInfo { + public bool UpdateAvailable { get; set; } + public Version CurrentVersion { get; set; } + public Version LatestVersion { get; set; } + public string ReleaseUrl { get; set; } + public string ErrorMessage { get; set; } + } + + public static async Task CheckForUpdate() + { + var updateInfo = new UpdateInfo + { + ReleaseUrl = "https://github.com/jexuswebserver/JexusManager/releases", + CurrentVersion = Assembly.GetExecutingAssembly().GetName().Version + }; + string version = null; var previous = ServicePointManager.SecurityProtocol; try @@ -24,8 +39,8 @@ public static async Task FindUpdate() var releases = await client.Repository.Release.GetAll("jexuswebserver", "JexusManager"); if (releases.Count == 0) { - DialogHelper.MessageBoxShow("No update is found."); - return; + updateInfo.ErrorMessage = "No update is found."; + return updateInfo; } var recent = releases[0]; @@ -33,31 +48,24 @@ public static async Task FindUpdate() } catch (Exception) { - DialogHelper.MessageBoxShow("Cannot connect to GitHub. Will open https://github.com/jexuswebserver/JexusManager/releases."); - DialogHelper.ProcessStart("https://github.com/jexuswebserver/JexusManager/releases"); - return; + updateInfo.ErrorMessage = "Cannot connect to GitHub."; + return updateInfo; } finally { ServicePointManager.SecurityProtocol = previous; } - Version latest; - if (!Version.TryParse(version, out latest)) - { - DialogHelper.MessageBoxShow("No update is found."); - return; - } - - var current = Assembly.GetExecutingAssembly().GetName().Version; - if (current >= latest) + if (!Version.TryParse(version, out Version latest)) { - DialogHelper.MessageBoxShow($"{current} is in use. No update is found, and {latest} is latest release."); - return; + updateInfo.ErrorMessage = "No update is found."; + return updateInfo; } - DialogHelper.MessageBoxShow($"{current} is in use. An update ({latest}) is available. Will open https://github.com/jexuswebserver/JexusManager/releases."); - DialogHelper.ProcessStart("https://github.com/jexuswebserver/JexusManager/releases"); + updateInfo.LatestVersion = latest; + updateInfo.UpdateAvailable = updateInfo.CurrentVersion < latest; + + return updateInfo; } } } diff --git a/JexusManager/Wizards/ConnectionWizard/CredentialsPage.cs b/JexusManager/Wizards/ConnectionWizard/CredentialsPage.cs index 637f6e5b..fffe1651 100644 --- a/JexusManager/Wizards/ConnectionWizard/CredentialsPage.cs +++ b/JexusManager/Wizards/ConnectionWizard/CredentialsPage.cs @@ -5,20 +5,20 @@ namespace JexusManager.Wizards.ConnectionWizard { using System; - using System.Diagnostics; - using System.IO; - using System.Net.Security; - using System.Text; - using System.Threading; using System.Windows.Forms; - - using JexusManager.Dialogs; - - using Microsoft.Web.Administration; + using Microsoft.Extensions.Logging; + using JexusManager; using Microsoft.Web.Management.Client.Win32; + using Microsoft.Web.Administration; + using System.Threading; + using System.Net.Security; + using JexusManager.Dialogs; + using System.Text; public partial class CredentialsPage : WizardPage { + private static readonly ILogger _logger = LogHelper.GetLogger("CredentialsPage"); + public CredentialsPage() { InitializeComponent(); @@ -138,21 +138,26 @@ private bool OpenConnection(SynchronizationContext context) } catch (Exception ex) { - Debug.WriteLine(ex); - File.WriteAllText(DialogHelper.DebugLog, ex.ToString()); - var last = ex; - while (last is AggregateException) - { - last = last.InnerException; - } - - var message = new StringBuilder(); - message.AppendLine("Could not connect to the specified computer.") - .AppendLine() - .AppendFormat("Details: {0}", last?.Message); - service?.ShowMessage(message.ToString(), Text, MessageBoxButtons.OK, MessageBoxIcon.Error); + RaiseError(ex); return false; } } + + private void RaiseError(Exception ex) + { + _logger.LogError(ex, "Error in credentials page"); + var service = (IManagementUIService)GetService(typeof(IManagementUIService)); + var last = ex; + while (last is AggregateException) + { + last = last.InnerException; + } + + var message = new StringBuilder(); + message.AppendLine("Could not connect to the specified computer.") + .AppendLine() + .AppendFormat("Details: {0}", last?.Message); + service?.ShowMessage(message.ToString(), Text, MessageBoxButtons.OK, MessageBoxIcon.Error); + } } } diff --git a/Microsoft.Web.Administration/AspNetCoreHelper.cs b/Microsoft.Web.Administration/AspNetCoreHelper.cs index 7800b681..ccb2f160 100644 --- a/Microsoft.Web.Administration/AspNetCoreHelper.cs +++ b/Microsoft.Web.Administration/AspNetCoreHelper.cs @@ -6,14 +6,21 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; using System.Xml.Linq; using System.Xml.XPath; +using JexusManager; +using Microsoft.Extensions.Logging; namespace Microsoft.Web.Administration { internal static class AspNetCoreHelper { - public static void FixConfigFile(string fileName) + private static readonly ILogger _logger = LogHelper.GetLogger("AspNetCoreHelper"); + + internal static void FixConfigFile(string fileName) { var doc = XDocument.Load(fileName); var virturalDirectories = doc.Descendants("virtualDirectory"); @@ -198,7 +205,7 @@ public static void InjectEnvironmentVariables(Site site, ProcessStartInfo startI .FirstOrDefault(); if (devServerVersion == null) { - Debug.WriteLine($"Blazor dev server missing"); + _logger.LogWarning("Blazor dev server missing in project {Project}", project); return; } @@ -208,7 +215,7 @@ public static void InjectEnvironmentVariables(Site site, ProcessStartInfo startI int index = input.IndexOf("net"); if (index == -1) { - Debug.WriteLine($"Unknown framework {input}"); + _logger.LogWarning("Unknown framework {Framework} in project {Project}", input, project); return; } @@ -219,7 +226,7 @@ public static void InjectEnvironmentVariables(Site site, ProcessStartInfo startI var devServerLibrary = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages", "microsoft.aspnetcore.components.webassembly.devserver", devServerVersion, "tools", "blazor-devserver.dll"); if (!File.Exists(devServerLibrary)) { - Debug.WriteLine($"Cannot find Blazor dev server library {devServerVersion}"); + _logger.LogWarning("Cannot find Blazor dev server library version {Version}", devServerVersion); return; } @@ -236,7 +243,7 @@ public static void InjectEnvironmentVariables(Site site, ProcessStartInfo startI int index = input.IndexOf("net"); if (index == -1) { - Debug.WriteLine($"Unknown framework {input}"); + _logger.LogWarning("Unknown framework {Framework} in project {Project}", input, project); return; } @@ -255,7 +262,7 @@ public static void InjectEnvironmentVariables(Site site, ProcessStartInfo startI } else { - Debug.WriteLine($"impossible ASP.NET Core version {version}"); + _logger.LogInformation("ASP.NET Core {Version} is older than baseline {Baseline}", version, baseVersion); } } else @@ -274,7 +281,7 @@ public static void InjectEnvironmentVariables(Site site, ProcessStartInfo startI var files = Directory.GetFiles(Path.Combine(root, "bin", "Debug", input), "*.dll"); if (files.Length == 1) { - Debug.WriteLine($"Didn't find the expected assembly. Choose another instead."); + _logger.LogDebug("Expected assembly not found, using alternative: {Assembly}", files[0]); primary = files[0]; } else @@ -285,7 +292,7 @@ public static void InjectEnvironmentVariables(Site site, ProcessStartInfo startI if (primary == null) { - Debug.WriteLine($"Didn't find compiled assembly for {input}"); + _logger.LogWarning("Could not find compiled assembly for framework {Framework} in {Path}", input, root); return; } @@ -294,7 +301,7 @@ public static void InjectEnvironmentVariables(Site site, ProcessStartInfo startI if (!File.Exists(file)) { // Not VS 15.2 and above - Debug.WriteLine($"Didn't detect VS 15.2 or above"); + _logger.LogWarning("Visual Studio 15.2 or above not detected. VSWhere not found at {Path}", file); return; } @@ -311,7 +318,7 @@ public static void InjectEnvironmentVariables(Site site, ProcessStartInfo startI var launcher = $@"{folder}\Common7\IDE\Extensions\Microsoft\Web Tools\ProjectSystem\VSIISExeLauncher.exe"; if (!File.Exists(launcher)) { - Debug.WriteLine($"Didn't detect VSIISExeLauncher"); + _logger.LogWarning("VSIISExeLauncher not found at {Path}", launcher); return; } @@ -325,6 +332,7 @@ public static void InjectEnvironmentVariables(Site site, ProcessStartInfo startI private static string RestoreAndBuild(string root) { var dotnet = Environment.ExpandEnvironmentVariables(@"%ProgramFiles%\dotnet\dotnet.exe"); + _logger.LogInformation("Running dotnet restore in {Path}", root); var restore = Process.Start(new ProcessStartInfo { FileName = dotnet, @@ -333,6 +341,8 @@ private static string RestoreAndBuild(string root) }); Debug.Assert(restore != null, nameof(restore) + " != null"); restore.WaitForExit(); + + _logger.LogInformation("Running dotnet build in {Path}", root); var build = Process.Start(new ProcessStartInfo { FileName = dotnet, diff --git a/Microsoft.Web.Administration/ConfigurationAttribute.cs b/Microsoft.Web.Administration/ConfigurationAttribute.cs index 855e3775..8c69542d 100644 --- a/Microsoft.Web.Administration/ConfigurationAttribute.cs +++ b/Microsoft.Web.Administration/ConfigurationAttribute.cs @@ -3,15 +3,19 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using Microsoft.Extensions.Logging; +using JexusManager; using System.Diagnostics; -using System.Security.Cryptography; using System.Runtime.InteropServices; +using System.Security.Cryptography; namespace Microsoft.Web.Administration { [DebuggerDisplay("{Name}")] - public class ConfigurationAttribute + public sealed class ConfigurationAttribute { + private static readonly ILogger _logger = LogHelper.GetLogger("ConfigurationAttribute"); + private object _value; private readonly ConfigurationElement _element; @@ -240,26 +244,10 @@ private string Encrypt(string value) } catch (Exception ex) { - // If encryption fails with the selected provider, try a fallback approach - System.Diagnostics.Debug.WriteLine($"Error encrypting with {selectedProvider}: {ex.Message}"); - - // Create a temporary RSA provider and encrypt with it - try - { - using var rsa = new RSACryptoServiceProvider(2048); - byte[] dataBytes = System.Text.Encoding.Unicode.GetBytes(value); - byte[] encryptedBytes = rsa.Encrypt(dataBytes, false); - string encryptedValue = Convert.ToBase64String(encryptedBytes); - - // Just return the encrypted value without the [enc:] wrapper - // This will treat it as cleartext later but at least it's obscured - return value; - } - catch - { - // If all encryption attempts fail, return the original value - return value; - } + _logger.LogError(ex, "Error encrypting with provider {Provider}", selectedProvider); + + // If all encryption attempts fail, return the original value + return value; } } } diff --git a/Microsoft.Web.Administration/ConfigurationAttributeSchema.cs b/Microsoft.Web.Administration/ConfigurationAttributeSchema.cs index 3c47e4d7..528d08f9 100644 --- a/Microsoft.Web.Administration/ConfigurationAttributeSchema.cs +++ b/Microsoft.Web.Administration/ConfigurationAttributeSchema.cs @@ -4,17 +4,21 @@ using System; using System.Xml.Linq; +using Microsoft.Extensions.Logging; +using JexusManager; using System.Diagnostics; -using System.Globalization; -using System.Reflection; using System.Runtime.InteropServices; using System.Threading; +using System.Reflection; +using System.Globalization; namespace Microsoft.Web.Administration { [DebuggerDisplay("{Name}[{Type}]")] public sealed class ConfigurationAttributeSchema { + private static readonly ILogger _logger = LogHelper.GetLogger("ConfigurationAttributeSchema"); + private ConfigurationEnumValueCollection _collection; private ConfigurationValidatorBase _validator; @@ -220,7 +224,7 @@ public void CreateValidator() } catch (MissingMethodException) { - Debug.WriteLine($"type: {ValidationType}; parameter: {ValidationParameter}; is null ? {ValidationParameter == null}"); + _logger.LogDebug($"type: {ValidationType}; parameter: {ValidationParameter}; is null ? {ValidationParameter == null}"); throw; } } diff --git a/Microsoft.Web.Administration/FileContext.cs b/Microsoft.Web.Administration/FileContext.cs index be1cea08..8ccaba8e 100644 --- a/Microsoft.Web.Administration/FileContext.cs +++ b/Microsoft.Web.Administration/FileContext.cs @@ -3,20 +3,23 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Linq; -using System.Runtime.InteropServices; using System.Xml; using System.Xml.Linq; +using Microsoft.Extensions.Logging; +using JexusManager; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Linq; using System.Xml.XPath; +using System.Diagnostics; namespace Microsoft.Web.Administration { - [DebuggerDisplay("{FileName}[{Location}]")] internal sealed class FileContext : IEquatable { + private static readonly ILogger _logger = LogHelper.GetLogger("FileContext"); + private readonly ServerManager _server; private readonly object _locker = new object(); private readonly bool _doNotThrow; @@ -135,51 +138,51 @@ public void Save() lock (_locker) { - if (AppHost) + try { - // TODO: load settings from applicationHost.config. - var historyFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Jexus Manager", - "history"); - int last = 0; - if (!Directory.Exists(historyFolder)) - { - Directory.CreateDirectory(historyFolder); - } - else + if (AppHost) { - var existing = new DirectoryInfo(historyFolder).GetDirectories(); - last = - existing.Select(found => found.Name.Substring("CFGHISTORY_".Length)) - .Select(int.Parse) - .Concat(new[] { last }) - .Max(); - } + // TODO: load settings from applicationHost.config. + var historyFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Jexus Manager", + "history"); + int last = 0; + if (!Directory.Exists(historyFolder)) + { + Directory.CreateDirectory(historyFolder); + } + else + { + var existing = new DirectoryInfo(historyFolder).GetDirectories(); + last = + existing.Select(found => found.Name.Substring("CFGHISTORY_".Length)) + .Select(int.Parse) + .Concat(new[] { last }) + .Max(); + } - last++; - var revisionFolder = Path.Combine(historyFolder, "CFGHISTORY_" + last.ToString("D10")); - if (!Directory.Exists(revisionFolder)) - { - Directory.CreateDirectory(revisionFolder); - } + last++; + var revisionFolder = Path.Combine(historyFolder, "CFGHISTORY_" + last.ToString("D10")); + if (!Directory.Exists(revisionFolder)) + { + Directory.CreateDirectory(revisionFolder); + } - var fileName = Path.GetFileName(FileName); - if (fileName != null) - { - File.Copy(FileName, Path.Combine(revisionFolder, fileName)); + var fileName = Path.GetFileName(FileName); + if (fileName != null) + { + File.Copy(FileName, Path.Combine(revisionFolder, fileName)); + } } - } - if (_document != null) - { - try + if (_document != null) { using var stream = File.Open(FileName, FileMode.Create, FileAccess.Write, FileShare.None); _document.Save(stream); } - catch (SystemException ex) - { - Debug.WriteLine(ex); - } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error saving file {FileName}", FileName); } } } @@ -371,7 +374,7 @@ private void LoadSchema(XDocument document, string fileName) if (found == null) { found = new SectionSchema(name, element, fileName); - Debug.Assert(name != null, nameof(name) + " != null"); + System.Diagnostics.Debug.Assert(name != null, nameof(name) + " != null"); _sectionSchemas.Add(name, found); } diff --git a/Microsoft.Web.Administration/IisExpressServerManager.cs b/Microsoft.Web.Administration/IisExpressServerManager.cs index 41809503..fd25c2ee 100644 --- a/Microsoft.Web.Administration/IisExpressServerManager.cs +++ b/Microsoft.Web.Administration/IisExpressServerManager.cs @@ -9,12 +9,16 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using JexusManager; +using Microsoft.Extensions.Logging; using Exception = System.Exception; namespace Microsoft.Web.Administration { public sealed class IisExpressServerManager : ServerManager { + private static readonly ILogger _logger = LogHelper.GetLogger("IisExpressServerManager"); + public Version Version { get; } public string PrimaryExecutable { get; } @@ -87,12 +91,12 @@ internal override bool GetSiteState(Site site) StartInfo = new ProcessStartInfo { Verb = site.Bindings.ElevationRequired && !PublicNativeMethods.IsProcessElevated - ? "runas" - : null, + ? "runas" + : null, UseShellExecute = true, FileName = "cmd", Arguments = - $"/c \"\"{CertificateInstallerLocator.FileName}\" /config:\"{site.FileContext.FileName}\" /siteId:{site.Id}\"", + $"/c \"\"{CertificateInstallerLocator.FileName}\" /config:\"{site.FileContext.FileName}\" /siteId:{site.Id}\"", CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden } @@ -107,22 +111,19 @@ internal override bool GetSiteState(Site site) // elevation is cancelled. if (ex.NativeErrorCode != (int)Windows.Win32.Foundation.WIN32_ERROR.ERROR_CANCELLED) { - Debug.WriteLine(ex); - Debug.WriteLine($"native {ex.NativeErrorCode}"); - // throw; + _logger.LogWarning(ex, "Win32 error getting site state. Native error code: {Code}", ex.NativeErrorCode); } } catch (InvalidOperationException ex) { if (ex.HResult != NativeMethods.NoProcessAssociated) { - Debug.WriteLine(ex); - Debug.WriteLine($"hresult {ex.HResult}"); + _logger.LogError(ex, "Error getting site state. HResult: {HResult}", ex.HResult); } } catch (Exception ex) { - Debug.WriteLine(ex); + _logger.LogError(ex, "Unexpected error getting site state"); } return true; @@ -236,14 +237,12 @@ internal override void Stop(Site site) // elevation is cancelled. if (ex.NativeErrorCode != (int)Windows.Win32.Foundation.WIN32_ERROR.ERROR_CANCELLED) { - Debug.WriteLine(ex); - Debug.WriteLine($"native {ex.NativeErrorCode}"); - // throw; + _logger.LogError(ex, "Win32 error stopping site. Native error code: {Code}", ex.NativeErrorCode); } } catch (Exception ex) { - Debug.WriteLine(ex); + _logger.LogError(ex, "Error stopping site", ex); } } diff --git a/Microsoft.Web.Configuration.AppHostFileProvider/AesEncryptionProvider.cs b/Microsoft.Web.Configuration.AppHostFileProvider/AesEncryptionProvider.cs index d2be43e8..c3eb547e 100644 --- a/Microsoft.Web.Configuration.AppHostFileProvider/AesEncryptionProvider.cs +++ b/Microsoft.Web.Configuration.AppHostFileProvider/AesEncryptionProvider.cs @@ -60,12 +60,10 @@ public unsafe static string AesDecrypt(byte[] encrypted, string keyContainer, st dwFlags = CRYPT_KEY_FLAGS.CRYPT_OAEP; } - fixed (byte* sessionPtr = sessionKey) + ReadOnlySpan sessionPtr = sessionKey; + if (!Windows.Win32.PInvoke.CryptImportKey(handle, sessionPtr, hKey, dwFlags, out hEncryptKey)) { - if (!Windows.Win32.PInvoke.CryptImportKey(handle, sessionPtr, (uint)sessionKey.Length, hKey, dwFlags, out hEncryptKey)) - { - throw new Win32Exception(Marshal.GetLastWin32Error()); - } + throw new Win32Exception(Marshal.GetLastWin32Error()); } byte[] array = new byte[encrypted.Length]; @@ -122,36 +120,30 @@ public unsafe static byte[] AesEncrypt(string data, string keyContainer, string dwFlags = CRYPT_KEY_FLAGS.CRYPT_OAEP; } - fixed (byte* sessionPtr = sessionKey) + ReadOnlySpan sessionPtr = sessionKey; + if (!Windows.Win32.PInvoke.CryptImportKey(handle, sessionPtr, hKey, dwFlags, out hEncryptKey)) { - if (!Windows.Win32.PInvoke.CryptImportKey(handle, sessionPtr, (uint)sessionKey.Length, hKey, dwFlags, out hEncryptKey)) - { - throw new Win32Exception(Marshal.GetLastWin32Error()); - } + throw new Win32Exception(Marshal.GetLastWin32Error()); } uint pdwDataLen = (uint)array.Length; - fixed (byte* arrayPtr = array) + ReadOnlySpan arrayPtr = array; + if (!Windows.Win32.PInvoke.CryptEncrypt(hEncryptKey, 0, Final: true, 0u, arrayPtr, ref pdwDataLen)) { - if (!Windows.Win32.PInvoke.CryptEncrypt(hEncryptKey, 0, Final: true, 0u, arrayPtr, ref pdwDataLen, 0)) + int lastWin32Error = Marshal.GetLastWin32Error(); + if (lastWin32Error != 234) { - int lastWin32Error = Marshal.GetLastWin32Error(); - if (lastWin32Error != 234) - { - throw new Win32Exception(lastWin32Error); - } + throw new Win32Exception(lastWin32Error); } } byte[] array2 = new byte[pdwDataLen]; array.CopyTo(array2, 0); pdwDataLen = (uint)array.Length; - fixed (byte* array2Ptr = array2) + ReadOnlySpan array2Ptr = array2; + if (!Windows.Win32.PInvoke.CryptEncrypt(hEncryptKey, 0, Final: true, 0u, array2Ptr, ref pdwDataLen)) { - if (!Windows.Win32.PInvoke.CryptEncrypt(hEncryptKey, 0, Final: true, 0u, array2Ptr, ref pdwDataLen, (uint)array2.Length)) - { - throw new Win32Exception(Marshal.GetLastWin32Error()); - } + throw new Win32Exception(Marshal.GetLastWin32Error()); } return array2; diff --git a/Microsoft.Web.Configuration.AppHostFileProvider/LoggingService.cs b/Microsoft.Web.Configuration.AppHostFileProvider/LoggingService.cs new file mode 100644 index 00000000..8878d334 --- /dev/null +++ b/Microsoft.Web.Configuration.AppHostFileProvider/LoggingService.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace JexusManager +{ + public static class LogHelper + { + private static ILoggerFactory _loggerFactory; + + public static void Initialize(ILoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + } + + public static ILogger GetLogger() + { + if (_loggerFactory == null) + { + throw new InvalidOperationException("LoggerFactory has not been initialized."); + } + + return _loggerFactory.CreateLogger(); + } + + public static ILogger GetLogger(string categoryName) + { + if (_loggerFactory == null) + { + throw new InvalidOperationException("LoggerFactory has not been initialized."); + } + + return _loggerFactory.CreateLogger(categoryName); + } + } +} diff --git a/Microsoft.Web.Configuration.AppHostFileProvider/Microsoft.Web.Configuration.AppHostFileProvider.csproj b/Microsoft.Web.Configuration.AppHostFileProvider/Microsoft.Web.Configuration.AppHostFileProvider.csproj index 99824381..951fc404 100644 --- a/Microsoft.Web.Configuration.AppHostFileProvider/Microsoft.Web.Configuration.AppHostFileProvider.csproj +++ b/Microsoft.Web.Configuration.AppHostFileProvider/Microsoft.Web.Configuration.AppHostFileProvider.csproj @@ -12,6 +12,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Microsoft.Web.Configuration.AppHostFileProvider/NativeMethods.cs b/Microsoft.Web.Configuration.AppHostFileProvider/NativeMethods.cs index e21ead44..d6bd8ae5 100644 --- a/Microsoft.Web.Configuration.AppHostFileProvider/NativeMethods.cs +++ b/Microsoft.Web.Configuration.AppHostFileProvider/NativeMethods.cs @@ -88,6 +88,7 @@ public static string CngDecrypt(byte[] encrypted, string keyContainer) uint pcbData = (uint)encrypted.Length; byte[] array = new byte[pcbData]; encrypted.CopyTo(array, 0); + if (IisCngDecrypt(keyContainer, array, ref pcbData) != 0L) { throw new Win32Exception(Marshal.GetLastWin32Error()); } diff --git a/Microsoft.Web.Configuration.AppHostFileProvider/NativeMethods.json b/Microsoft.Web.Configuration.AppHostFileProvider/NativeMethods.json index ba907b86..66fff887 100644 --- a/Microsoft.Web.Configuration.AppHostFileProvider/NativeMethods.json +++ b/Microsoft.Web.Configuration.AppHostFileProvider/NativeMethods.json @@ -1,5 +1,8 @@ { "$schema": "https://aka.ms/CsWin32.schema.json", "public": false, - "emitSingleFile": true + "emitSingleFile": true, + "useSafeHandles": true, + "allowMarshaling": true, + "multiTargetingFriendlyAPIs": true } diff --git a/Microsoft.Web.Configuration.AppHostFileProvider/NativeMethods.txt b/Microsoft.Web.Configuration.AppHostFileProvider/NativeMethods.txt index e2151334..8465e079 100644 --- a/Microsoft.Web.Configuration.AppHostFileProvider/NativeMethods.txt +++ b/Microsoft.Web.Configuration.AppHostFileProvider/NativeMethods.txt @@ -7,8 +7,8 @@ CryptImportKey CryptGetUserKey CryptGenRandom PROV_RSA_AES -AT_KEYEXCHANGE -ERROR_MORE_DATA -NTE_EXISTS +CERT_KEY_SPEC +WIN32_ERROR +CRYPT_KEY_FLAGS CRYPT_NEWKEYSET -CRYPT_MACHINE_KEYSET +NTE_EXISTS