diff --git a/.gitignore b/.gitignore index 0df25f4..df53ff5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,7 @@ buildtasks artifacts robots.txt syntax: regexp -(?i)src/.*bin/.* \ No newline at end of file +(?i)src/.*bin/.* +.idea +/src/Orchard-Core/bin +/src/*/bin diff --git a/AddPathToPSModulePath.ps1 b/AddPathToPSModulePath.ps1 index a6fbc02..7bc9155 100644 --- a/AddPathToPSModulePath.ps1 +++ b/AddPathToPSModulePath.ps1 @@ -22,6 +22,7 @@ $paths = [Environment]::GetEnvironmentVariable("PSModulePath", "Machine").Split( if(!$paths.Contains($Path)) { [System.Environment]::SetEnvironmentVariable("PSModulePath", [string]::Join(";", $paths + $Path), "Machine") + [System.Environment]::SetEnvironmentVariable("LOMBIQ_UTILITY_SCRIPTS_PATH", $pwd.Path, "Machine") Write-Information "The path `"$Path`" was successfully added to the PSModulePath environment variable." } else diff --git a/OrchardCore/Reset-OrchardCoreApp/Reset-OrchardCoreApp.psm1 b/OrchardCore/Reset-OrchardCoreApp/Reset-OrchardCoreApp.psm1 index 773b7e2..13167d6 100644 --- a/OrchardCore/Reset-OrchardCoreApp/Reset-OrchardCoreApp.psm1 +++ b/OrchardCore/Reset-OrchardCoreApp/Reset-OrchardCoreApp.psm1 @@ -82,23 +82,21 @@ function Reset-OrchardCoreApp # Trying to find IIS Express and .NET host processes that run a Web Project with a matching name and terminate them. - $siteHostProcessFilter = "(Name = 'iisexpress.exe' or Name = 'dotnet.exe') and CommandLine like '%$siteName%'" - $siteHostProcesses = Get-WmiObject Win32_Process -Filter $siteHostProcessFilter + Import-Module "$env:LOMBIQ_UTILITY_SCRIPTS_PATH\src\Lombiq.UtilityScripts.Utilities\bin\Debug\netstandard2.0\Lombiq.UtilityScripts.Utilities.dll" + $siteHostProcesses = Get-ProcessByArgument $siteName - if ($siteHostProcesses -ne $null -or $siteHostProcesses.Count -gt 0) + if ($siteHostProcesses.Count -gt 0) { foreach ($siteHostProcess in $siteHostProcesses) { "Terminating application host process running `"$($siteHostProcess.CommandLine)`"!`n" - $siteHostProcess.Terminate() | Out-Null + $siteHostProcess.Process | Stop-Process } Start-Sleep 1 } - - # Delete App_Data if exists. $appDataPath = $("$WebProjectPath\App_Data") diff --git a/Utility-Scripts.sln b/Utility-Scripts.sln new file mode 100644 index 0000000..42b5ff1 --- /dev/null +++ b/Utility-Scripts.sln @@ -0,0 +1,69 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8EA34D28-4D2E-4E4E-BDE7-AC8E8C10AD18}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lombiq.UtilityScripts.Common", "src\Lombiq.UtilityScripts.Common\Lombiq.UtilityScripts.Common.csproj", "{CA3CF7A3-71B4-442E-9CF7-3982F4CE9490}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lombiq.UtilityScripts.SqlServer", "src\Lombiq.UtilityScripts.SqlServer\Lombiq.UtilityScripts.SqlServer.csproj", "{79B8C1DD-ADBD-4266-B89A-D938D0614D50}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lombiq.UtilityScripts.Utilities", "src\Lombiq.UtilityScripts.Utilities\Lombiq.UtilityScripts.Utilities.csproj", "{E39E4A3D-81FE-45CE-9A10-28DD5FB3B615}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CA3CF7A3-71B4-442E-9CF7-3982F4CE9490}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA3CF7A3-71B4-442E-9CF7-3982F4CE9490}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA3CF7A3-71B4-442E-9CF7-3982F4CE9490}.Debug|x64.ActiveCfg = Debug|Any CPU + {CA3CF7A3-71B4-442E-9CF7-3982F4CE9490}.Debug|x64.Build.0 = Debug|Any CPU + {CA3CF7A3-71B4-442E-9CF7-3982F4CE9490}.Debug|x86.ActiveCfg = Debug|Any CPU + {CA3CF7A3-71B4-442E-9CF7-3982F4CE9490}.Debug|x86.Build.0 = Debug|Any CPU + {CA3CF7A3-71B4-442E-9CF7-3982F4CE9490}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA3CF7A3-71B4-442E-9CF7-3982F4CE9490}.Release|Any CPU.Build.0 = Release|Any CPU + {CA3CF7A3-71B4-442E-9CF7-3982F4CE9490}.Release|x64.ActiveCfg = Release|Any CPU + {CA3CF7A3-71B4-442E-9CF7-3982F4CE9490}.Release|x64.Build.0 = Release|Any CPU + {CA3CF7A3-71B4-442E-9CF7-3982F4CE9490}.Release|x86.ActiveCfg = Release|Any CPU + {CA3CF7A3-71B4-442E-9CF7-3982F4CE9490}.Release|x86.Build.0 = Release|Any CPU + {79B8C1DD-ADBD-4266-B89A-D938D0614D50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79B8C1DD-ADBD-4266-B89A-D938D0614D50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79B8C1DD-ADBD-4266-B89A-D938D0614D50}.Debug|x64.ActiveCfg = Debug|Any CPU + {79B8C1DD-ADBD-4266-B89A-D938D0614D50}.Debug|x64.Build.0 = Debug|Any CPU + {79B8C1DD-ADBD-4266-B89A-D938D0614D50}.Debug|x86.ActiveCfg = Debug|Any CPU + {79B8C1DD-ADBD-4266-B89A-D938D0614D50}.Debug|x86.Build.0 = Debug|Any CPU + {79B8C1DD-ADBD-4266-B89A-D938D0614D50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79B8C1DD-ADBD-4266-B89A-D938D0614D50}.Release|Any CPU.Build.0 = Release|Any CPU + {79B8C1DD-ADBD-4266-B89A-D938D0614D50}.Release|x64.ActiveCfg = Release|Any CPU + {79B8C1DD-ADBD-4266-B89A-D938D0614D50}.Release|x64.Build.0 = Release|Any CPU + {79B8C1DD-ADBD-4266-B89A-D938D0614D50}.Release|x86.ActiveCfg = Release|Any CPU + {79B8C1DD-ADBD-4266-B89A-D938D0614D50}.Release|x86.Build.0 = Release|Any CPU + {E39E4A3D-81FE-45CE-9A10-28DD5FB3B615}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E39E4A3D-81FE-45CE-9A10-28DD5FB3B615}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E39E4A3D-81FE-45CE-9A10-28DD5FB3B615}.Debug|x64.ActiveCfg = Debug|Any CPU + {E39E4A3D-81FE-45CE-9A10-28DD5FB3B615}.Debug|x64.Build.0 = Debug|Any CPU + {E39E4A3D-81FE-45CE-9A10-28DD5FB3B615}.Debug|x86.ActiveCfg = Debug|Any CPU + {E39E4A3D-81FE-45CE-9A10-28DD5FB3B615}.Debug|x86.Build.0 = Debug|Any CPU + {E39E4A3D-81FE-45CE-9A10-28DD5FB3B615}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E39E4A3D-81FE-45CE-9A10-28DD5FB3B615}.Release|Any CPU.Build.0 = Release|Any CPU + {E39E4A3D-81FE-45CE-9A10-28DD5FB3B615}.Release|x64.ActiveCfg = Release|Any CPU + {E39E4A3D-81FE-45CE-9A10-28DD5FB3B615}.Release|x64.Build.0 = Release|Any CPU + {E39E4A3D-81FE-45CE-9A10-28DD5FB3B615}.Release|x86.ActiveCfg = Release|Any CPU + {E39E4A3D-81FE-45CE-9A10-28DD5FB3B615}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {CA3CF7A3-71B4-442E-9CF7-3982F4CE9490} = {8EA34D28-4D2E-4E4E-BDE7-AC8E8C10AD18} + {79B8C1DD-ADBD-4266-B89A-D938D0614D50} = {8EA34D28-4D2E-4E4E-BDE7-AC8E8C10AD18} + {E39E4A3D-81FE-45CE-9A10-28DD5FB3B615} = {8EA34D28-4D2E-4E4E-BDE7-AC8E8C10AD18} + EndGlobalSection +EndGlobal diff --git a/src/Lombiq.UtilityScripts.Common/Cmdlets/AsyncCmdletBase.cs b/src/Lombiq.UtilityScripts.Common/Cmdlets/AsyncCmdletBase.cs new file mode 100644 index 0000000..661a1ba --- /dev/null +++ b/src/Lombiq.UtilityScripts.Common/Cmdlets/AsyncCmdletBase.cs @@ -0,0 +1,62 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Lombiq.UtilityScripts.Common.Cmdlets +{ + public abstract class AsyncCmdletBase : PSCmdlet + { + protected abstract string CmdletName { get; } + + private ServiceProvider? _provider; + private IServiceProvider? _scopeProvider; + + protected IServiceProvider ServiceProvider => + _scopeProvider ?? _provider ?? throw new InvalidOperationException($"{nameof(BeginProcessing)} was not called!"); + + protected override void BeginProcessing() + { + var services = new ServiceCollection(); + services.AddLogging(options => options.AddConsole()); + services.AddSingleton(this); + services.AddSingleton(this); + + Configure(services); + _provider = services.BuildServiceProvider(); + } + + protected override void ProcessRecord() + { + try + { + if (_scopeProvider != null) + { + throw new InvalidOperationException("Overlapping scopes! This should not be possible."); + } + + using var scope = ServiceProvider.CreateScope(); + _scopeProvider = scope.ServiceProvider; + + ProcessRecordAsync().Wait(); + + _scopeProvider = null; + } + catch (Exception exception) + { + Error(exception, ErrorCategory.NotSpecified); + } + } + + protected override void EndProcessing() => _provider?.Dispose(); + + protected virtual void Configure(IServiceCollection services) { } + + protected abstract Task ProcessRecordAsync(); + + protected void Info(string message) => WriteInformation(new InformationRecord(message, CmdletName)); + protected void Error(Exception exception, ErrorCategory errorCategory) => + WriteError(new ErrorRecord(exception, exception.GetType().Name, errorCategory, CmdletName)); + } +} \ No newline at end of file diff --git a/src/Lombiq.UtilityScripts.Common/Lombiq.UtilityScripts.Common.csproj b/src/Lombiq.UtilityScripts.Common/Lombiq.UtilityScripts.Common.csproj new file mode 100644 index 0000000..6946b89 --- /dev/null +++ b/src/Lombiq.UtilityScripts.Common/Lombiq.UtilityScripts.Common.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.1 + enable + + + + + + + All + + + + + diff --git a/src/Lombiq.UtilityScripts.SqlServer/Cmdlets/SqlConnectionExtensions.cs b/src/Lombiq.UtilityScripts.SqlServer/Cmdlets/SqlConnectionExtensions.cs new file mode 100644 index 0000000..e4755da --- /dev/null +++ b/src/Lombiq.UtilityScripts.SqlServer/Cmdlets/SqlConnectionExtensions.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Data.SqlClient; + +namespace Lombiq.UtilityScripts.SqlServer.Cmdlets +{ + public static class SqlConnectionExtensions + { + public static SqlCommand GetCommand( + this SqlConnection connection, + string commandText, + IDictionary? parameters = null) + { + var command = connection.CreateCommand(); + command.CommandText = commandText; + + if (parameters != null) + { + foreach (var (name, value) in parameters) + { + command.Parameters.AddWithValue(name, value); + } + } + + return command; + } + + public static async IAsyncEnumerable YieldStringColumnAsync(this SqlCommand command, int columnId = 0) + { + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + yield return reader.GetString(columnId); + } + } + } +} \ No newline at end of file diff --git a/src/Lombiq.UtilityScripts.SqlServer/Lombiq.UtilityScripts.SqlServer.csproj b/src/Lombiq.UtilityScripts.SqlServer/Lombiq.UtilityScripts.SqlServer.csproj new file mode 100644 index 0000000..3b2d659 --- /dev/null +++ b/src/Lombiq.UtilityScripts.SqlServer/Lombiq.UtilityScripts.SqlServer.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.1 + enable + + + + + + + + + + + diff --git a/src/Lombiq.UtilityScripts.SqlServer/Services/SqlServerManager.cs b/src/Lombiq.UtilityScripts.SqlServer/Services/SqlServerManager.cs new file mode 100644 index 0000000..01c8920 --- /dev/null +++ b/src/Lombiq.UtilityScripts.SqlServer/Services/SqlServerManager.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Threading.Tasks; +using Lombiq.UtilityScripts.SqlServer.Cmdlets; +using Microsoft.Extensions.Logging; + +namespace Lombiq.UtilityScripts.SqlServer.Services +{ + public class SqlServerManager + { + private readonly ILogger _logger; + + public SqlServerManager(ILogger logger) + { + _logger = logger; + } + + public async Task CreateNewDatabase( + string sqlServerName, + string databaseName, + bool force = false, + string? userName = null, + string? password = null) + { + var connection = CreateNewConnection(sqlServerName, userName, password); + await connection.OpenAsync(); + + if (await TestDatabaseAsync(connection, databaseName)) + { + if (force) + { + _logger.LogWarning("Dropping database \"{0}\\{1}\"!", sqlServerName, databaseName); + await KillAllProcessesAsync(connection, databaseName); + await connection.GetCommand("DROP " + databaseName).ExecuteNonQueryAsync(); + } + else + { + _logger.LogWarning( + "A database with the name \"{0}\" already exists on the SQL Server at \"{1}\". Use the " + + "\"-Force\" switch to drop it and create a new database with that name.", + databaseName, + sqlServerName); + + return false; + } + } + + try + { + await connection.GetCommand($"CREATE DATABASE [{databaseName}];").ExecuteNonQueryAsync(); + } + catch (Exception exception) + { + throw new InvalidOperationException( + $"Could not create \"{sqlServerName}\\{databaseName}\"! ({exception.Message})", + exception); + } + + return true; + } + + public SqlConnection CreateNewConnection(string sqlServerName, string? userName = null, string? password = null) + { + var builder = new SqlConnectionStringBuilder("Server=" + sqlServerName); + + if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password)) + { + builder.Password = userName; + builder.UserID = password; + } + + return new SqlConnection(builder.ConnectionString); + } + + public async Task TestServerAsync(SqlConnection connection) + { + try + { + if (connection.State != ConnectionState.Open) + { + await connection.CloseAsync(); + await connection.OpenAsync(); + } + + var command = connection.CreateCommand(); + command.CommandText = "SELECT 1"; + return await command.ExecuteScalarAsync() is int response && response == 1; + } + catch + { + return false; + } + } + + public async Task TestDatabaseAsync(SqlConnection connection, string databaseName) + { + // If success it also ensures that the connection is open. + if (!await TestServerAsync(connection)) + { + throw new InvalidOperationException($"Could not find SQL Server for \"{nameof(connection.ConnectionString)}\"!"); + } + + await foreach (var name in connection.GetCommand("SELECT name FROM sys.databases").YieldStringColumnAsync()) + { + if (name?.Equals(databaseName, StringComparison.OrdinalIgnoreCase) == true) + { + return true; + } + } + + return false; + } + + private async Task KillAllProcessesAsync(SqlConnection connection, string databaseName) + { + if (databaseName == null) + { + throw new ArgumentNullException(nameof(databaseName)); + } + + const string columnName = "columnName"; + var commandText = connection.ServerVersion is { } serverVersion && new Version(serverVersion).Major == 8 + ? @$"SELECT DISTINCT req_spid as {columnName} + FROM master.dbo.syslockinfo + WHERE rsc_type = 2 AND rsc_dbid = db_id(@{nameof(databaseName)}) AND req_spid > 50" + : @$"SELECT DISTINCT request_session_id as {columnName} + FROM master.sys.dm_tran_locks + WHERE resource_type = 'DATABASE' AND resource_database_id = db_id(@{nameof(databaseName)})"; + + var targets = connection + .GetCommand(commandText, new Dictionary { [nameof(databaseName)] = databaseName }) + .YieldStringColumnAsync(); + + await foreach (var target in targets) await connection.GetCommand("KILL " + target).ExecuteNonQueryAsync(); + } + } +} \ No newline at end of file diff --git a/src/Lombiq.UtilityScripts.Utilities/Cmdlets/GetProcessByArgumentCmdletCommand.cs b/src/Lombiq.UtilityScripts.Utilities/Cmdlets/GetProcessByArgumentCmdletCommand.cs new file mode 100644 index 0000000..9efeb6a --- /dev/null +++ b/src/Lombiq.UtilityScripts.Utilities/Cmdlets/GetProcessByArgumentCmdletCommand.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Management; +using System.Management.Automation; +using System.Runtime.InteropServices; +using Lombiq.UtilityScripts.Utilities.Models; + +namespace Lombiq.UtilityScripts.Utilities.Cmdlets +{ + /// + /// Returns a collection of & command line argument pairs where it matches the search text + /// in the parameter (case-insensitive). + /// + [Cmdlet(VerbsCommon.Get, "ProcessByArgument")] + [OutputType(typeof(ExternalProcessWithArguments))] + public class GetProcessByArgumentCmdletCommand : Cmdlet + { + [Parameter( + Mandatory = true, + Position = 0, + HelpMessage = "The text to be searched (case-insensitive) in the process command line arguments.")] + public string Argument { get; set; } + + protected override void ProcessRecord() + { + try + { + var infos = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + ? ProcessLinux() + : ProcessWindows(); + + foreach (var info in infos) WriteObject(info); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + + private IEnumerable ProcessWindows() + { + var query = + $"SELECT ProcessId, CommandLine FROM Win32_Process " + + $"WHERE CommandLine LIKE '%{Argument}%'"; + + var list = new List(); + using (var searcher = new ManagementObjectSearcher(query)) + { + foreach (var result in searcher.Get()) + { + var processIdString = result["ProcessId"]?.ToString(); + var process = int.TryParse(processIdString, out var processId) + ? Process.GetProcessById(processId) + : null; + + if (process == null) continue; + + list.Add(new ExternalProcessWithArguments + { + Process = process, + CommandLine = result["CommandLine"]?.ToString() ?? string.Empty + }); + } + } + + return list; + } + + private IEnumerable ProcessLinux() + { + var argument = Argument.ToUpperInvariant(); + var list = new List(); + + foreach (var process in Process.GetProcesses()) + { + if (!File.Exists($"/proc/{process.Id}/cmdline")) continue; + var commandLine = File.ReadAllText($"/proc/{process.Id}/cmdline") ?? string.Empty; + + if (commandLine.ToUpperInvariant().Contains(argument)) + { + list.Add(new ExternalProcessWithArguments + { + Process = process, + CommandLine = commandLine, + }); + } + } + + return list; + } + } +} \ No newline at end of file diff --git a/src/Lombiq.UtilityScripts.Utilities/Lombiq.UtilityScripts.Utilities.csproj b/src/Lombiq.UtilityScripts.Utilities/Lombiq.UtilityScripts.Utilities.csproj new file mode 100644 index 0000000..21a8d81 --- /dev/null +++ b/src/Lombiq.UtilityScripts.Utilities/Lombiq.UtilityScripts.Utilities.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + Lombiq.UtilityScripts.Utilities + + + + + All + + + + + diff --git a/src/Lombiq.UtilityScripts.Utilities/Models/ExternalProcessWithArguments.cs b/src/Lombiq.UtilityScripts.Utilities/Models/ExternalProcessWithArguments.cs new file mode 100644 index 0000000..ddf404d --- /dev/null +++ b/src/Lombiq.UtilityScripts.Utilities/Models/ExternalProcessWithArguments.cs @@ -0,0 +1,10 @@ +using System.Diagnostics; + +namespace Lombiq.UtilityScripts.Utilities.Models +{ + public class ExternalProcessWithArguments + { + public Process Process { get; set; } + public string CommandLine { get; set; } + } +} \ No newline at end of file