From d9ecaa6df37fa3c3a35c0c110a336f6d8dd1d52a Mon Sep 17 00:00:00 2001 From: LabodiDavid Date: Tue, 4 Feb 2025 13:27:46 +0100 Subject: [PATCH] Initial commit --- Install-Queue-Worker-Service.ps1 | 91 +++++++++++++++++++++++ LICENSE | 21 ++++++ README.md | 94 +++++++++++++++++++++++ Uninstall-Queue-Worker-Service.ps1 | 77 +++++++++++++++++++ queue-worker.ps1 | 115 +++++++++++++++++++++++++++++ 5 files changed, 398 insertions(+) create mode 100644 Install-Queue-Worker-Service.ps1 create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Uninstall-Queue-Worker-Service.ps1 create mode 100644 queue-worker.ps1 diff --git a/Install-Queue-Worker-Service.ps1 b/Install-Queue-Worker-Service.ps1 new file mode 100644 index 0000000..5f07537 --- /dev/null +++ b/Install-Queue-Worker-Service.ps1 @@ -0,0 +1,91 @@ +<# + Copyright (c) 2025 Dávid Lábodi + Licensed under the MIT License. See LICENSE file in the project root for full license information. +#> + +$ErrorActionPreference = "Stop" + +# Configuration +$serviceName = "LaravelQueueWorker" +$workerScriptName = "queue-worker.ps1" +$serviceDisplayName = "Laravel Queue Worker" +$serviceDescription = "Runs Laravel queue worker process" + +$scriptDir = $PSScriptRoot # Need to place this script too in the laravel project root directory! +$logDir = Join-Path $scriptDir -ChildPath "storage\logs" +$nssmDir = Join-Path $scriptDir "nssm\nssm-2.24-101-g897c7ad\win64" +$nssmExe = Join-Path $nssmDir "nssm.exe" +$workerScriptPath = Join-Path $scriptDir $workerScriptName + +if (-not (Test-Path $LogDir)) { + Write-Host "Laravel log directory not found, ensure the script is in the laravel root directory." -ForegroundColor Red + Read-Host + exit 1 +} + +# Check if the script is running as an administrator +function Test-Admin { + $currentUser = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) + return $currentUser.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +# Restart the script as an administrator if not already running as one +if (-not (Test-Admin)) { + Write-Host "This script requires administrator privileges." -ForegroundColor Yellow + Write-Host "Restarting script with elevated permissions..." -ForegroundColor Yellow + + # Start a new PowerShell process with elevated privileges + $newProcess = Start-Process powershell -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs -PassThru + + # Wait for the new process to exit + $newProcess.WaitForExit() + + # Exit the current script + exit +} + +# Download and extract NSSM if missing +if (-not (Test-Path $nssmExe)) { + Write-Host "Downloading NSSM..." + $nssmZipUrl = "https://nssm.cc/ci/nssm-2.24-101-g897c7ad.zip" + $zipPath = Join-Path $env:TEMP "nssm.zip" + + try { + Invoke-WebRequest -Uri $nssmZipUrl -OutFile $zipPath + Expand-Archive -Path $zipPath -DestinationPath $nssmDir + } + finally { + if (Test-Path $zipPath) { Remove-Item $zipPath } + } +} + +# Verify worker script exists +if (-not (Test-Path $workerScriptPath)) { + throw "Worker script ($workerScriptName) not found in script directory!" +} + +# Check existing service +if (Get-Service $serviceName -ErrorAction SilentlyContinue) { + throw "Service $serviceName already exists! Uninstall it first." +} + +# Get PowerShell path +$powerShellPath = (Get-Command "powershell.exe").Path + +# Install service +& $nssmExe install $serviceName $powerShellPath "-ExecutionPolicy Bypass -File `"$workerScriptPath`"" +& $nssmExe set $serviceName DisplayName $serviceDisplayName +& $nssmExe set $serviceName Description $serviceDescription +& $nssmExe set $serviceName AppDirectory $scriptDir +& $nssmExe set $serviceName Start SERVICE_AUTO_START +& $nssmExe set $serviceName AppStdout (Join-Path $logDir "queue-service.log") +& $nssmExe set $serviceName AppStderr (Join-Path $logDir "queue-service-error.log") +Write-Host "" +Write-Host "===========================================" +Start-Service $serviceName +Write-Host "Service installed and started successfully!" -ForeGroundColor Green +Write-Host "Service Name: $serviceName" -ForeGroundColor Green +Write-Host "Worker Script: $workerScriptPath" -ForeGroundColor Green +Write-Host "===========================================" +Write-Host "Don't forget to add NSSM directory ($($nssmDir)) to .gitignore if this is a development instance!" +Read-Host \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c5ac2e5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Dávid Lábodi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d4b27d --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# Laravel Queue Worker Windows Service + +**PowerShell scripts** to install/uninstall a **Laravel Queue Worker Wrapper Script as a Windows service** using [NSSM (Non-Sucking Service Manager)](https://nssm.cc). + +## Features + +- 🛠️ Automated easy service installation/uninstallation +- 🔄 **Auto-restarting worker** with configurable delay +- 📅 **Time-stamped logging** integrated with Laravel's log system +- 📊 **Dual logging** (file + console if wrapper run directly) with ERROR tagging +- 🚦 **Process monitoring** with output/error stream handling +- 📁 Automated NSSM setup (downloaded on first run) +- 📝 Integrated logging for service operations (NSSM) +- 🔄 **Automatic service restart** on failure (NSSM) +- 🛠️ **Self-contained configuration** with automatic path detection + +## File Structure +``` +{LARAVEL_ROOT}/ + ├── Install-Queue-Worker-Service.ps1 # Service installation script + ├── Uninstall-Queue-Worker-Service.ps1 # Service removal script + ├── queue-worker.ps1 # Worker process wrapper, this is what the service run + └── /storage/logs/ # Laravel logs (auto-used) + └── queue-worker.txt # Real-time logging by worker wrapper + ├── queue-worker.log # Real-time logging by worker wrapper + ├── queue-service.log # Service output logs (auto-created, handled by NSSM) + └── queue-service-error.log # Service error logs (auto-created, handled by NSSM) +``` + +## Installation +1. **Clone repository:** +````powershell +git clone https://github.com/labodidavid/laravel-queue-service-win.git +```` +2. **Place scripts** in your Laravel root directory +3. **Run as Administrator**: +```powershell +# Install service +.\Install-Queue-Worker-Service.ps1 +``` +4. **In development environments:** Add `/nssm` directory or the scripts to `.gitignore` + +**Uninstall** when needed: +```powershell +.\Uninstall-Service.ps1 +``` +Alternatively you can run the worker wrapper without the service install: +```powershell +# Run worker directly (no service) +.\queue-worker.ps1 +``` + +## Customization + +Edit these in `queue-worker.ps1`: +```powershell +# Worker arguments (add --queue, --sleep, etc.) +$CommandArgs = "artisan queue:work --tries=3 --timeout=900" + +# Restart delay (seconds) +$RestartDelay = 10 + +# Log location (default: storage/logs/queue-worker.txt) +$LogPath = "C:\custom\logs\worker.log" +``` + + +## Troubleshooting +- *Permission errors*: Ensure write access to `storage/logs` +- *Missing PHP*: Verify PHP is in system PATH +- *Service not starting*: Check `Event Viewer → Windows Logs → Application or ` or `queue-service.log` + +## Requirements +- Windows Server 2012+ / Windows 10+ +- PowerShell 5.1+ +- An initialized Laravel project +- NSSM (auto-downloaded by install script) +## Contributing + +1. Fork the repository + +2. Create a feature branch (`git checkout -b feature/improvement`) + +3. Commit changes (`git commit -am 'Add some feature'`) + +4. Push to branch (`git push origin feature/improvement`) + +5. Open a Pull Request + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) \ No newline at end of file diff --git a/Uninstall-Queue-Worker-Service.ps1 b/Uninstall-Queue-Worker-Service.ps1 new file mode 100644 index 0000000..7e5b64e --- /dev/null +++ b/Uninstall-Queue-Worker-Service.ps1 @@ -0,0 +1,77 @@ +<# + Copyright (c) 2025 Dávid Lábodi + Licensed under the MIT License. See LICENSE file in the project root for full license information. +#> + +$ErrorActionPreference = "Stop" + +# Configuration +$serviceName = "LaravelQueueWorker" +$scriptDir = $PSScriptRoot # Define $scriptDir as the directory where the script is located +$nssmDir = Join-Path $scriptDir "nssm\nssm-2.24-101-g897c7ad\win64" +$nssmExe = Join-Path $nssmDir "nssm.exe" + +# Check if the script is running as an administrator +function Test-Admin { + $currentUser = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) + return $currentUser.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +# Restart the script as an administrator if not already running as one +if (-not (Test-Admin)) { + Write-Host "This script requires administrator privileges." -ForegroundColor Yellow + Write-Host "Restarting script with elevated permissions..." -ForegroundColor Yellow + + # Start a new PowerShell process with elevated privileges + $newProcess = Start-Process powershell -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs -PassThru + + # Wait for the new process to exit + $newProcess.WaitForExit() + + # Exit the current script + exit +} + +# If the script reaches this point, it is running as an administrator +Write-Host "Running with administrator privileges." -ForegroundColor Green + +# Check if NSSM exists +if (-not (Test-Path $nssmExe)) { + Write-Host "NSSM executable not found at $nssmExe. Please ensure NSSM is installed." -ForegroundColor Red + Read-Host "Press Enter to exit" + exit 1 +} + +# Check if service exists +$service = Get-Service $serviceName -ErrorAction SilentlyContinue +if (-not $service) { + Write-Host "Service $serviceName does not exist." -ForegroundColor Red + Read-Host "Press Enter to exit" + exit 0 +} + +# Stop and remove service +try { + if ($service.Status -eq "Running") { + Write-Host "Stopping service $serviceName..." -ForegroundColor Yellow + Stop-Service $serviceName -Force + Write-Host "Service stopped." -ForegroundColor Green + } + + Write-Host "Removing service $serviceName..." -ForegroundColor Yellow + & $nssmExe remove $serviceName confirm + Write-Host "Service uninstalled." -ForegroundColor Green +} +catch { + Write-Host "An error occurred: $_" -ForegroundColor Red + Write-Host "" + Write-Host "Ensure the script is running as admin privileges, and the service name is correct!" -ForegroundColor Red + Read-Host "Press Enter to exit" + exit 1 +} + +# Optional: Cleanup NSSM (comment out if you want to remove) +# Remove-Item $nssmDir -Recurse -Force +# Write-Host "NSSM uninstalled." -ForeGroundColor Green + +Read-Host "Press Enter to exit" \ No newline at end of file diff --git a/queue-worker.ps1 b/queue-worker.ps1 new file mode 100644 index 0000000..8923a35 --- /dev/null +++ b/queue-worker.ps1 @@ -0,0 +1,115 @@ +<# + Copyright (c) 2025 Dávid Lábodi + Licensed under the MIT License. See LICENSE file in the project root for full license information. +#> + +$startingHourMinute = Get-Date -Format "HHmm" # Script start hour and minute for include in the log file name + +# Configuration +$LaravelPath = $PSScriptRoot # Path to your Laravel project +$PhpPath = (Get-Command php.exe).Source # Path to your PHP executable +$LogDir = "$LaravelPath\storage\logs" # Path to the log dir +$LogPath = "$($LogDir)\queue-worker.txt" # Path to the default log file +$CommandArgs = "artisan queue:work --tries=3" # Artisan command arguments +$RestartDelay = 5 # Delay in seconds before restarting + + +if (-not (Test-Path $LogDir)) { + Write-Host "Laravel log directory not found, ensure the script is in the laravel root directory." -ForegroundColor Red + exit 1 +} + +function Log { + param ( + [string]$Message + ) + $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $FinalText = "[$($Timestamp)] $($Message)" + + # Write to the log file + $FinalText | Out-File -FilePath $LogPath -Append -Encoding utf8 + + # Optionally, display in the console + Write-Host $FinalText +} + +Log -Message "Starting, monitoring Laravel Queue Worker..." +Write-Host "Log directory: $LogDir" +Write-Host "" + +# Variable to store the current worker process +$WorkerProcess = $null +# Cleanup function to terminate the worker process +function Cleanup-Worker { + if ($WorkerProcess -and !$WorkerProcess.HasExited) { + Log -Message "Stopping artisan queue worker..." + $WorkerProcess.Kill() + } +} + +# Register cleanup on PowerShell session exit +Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action { Cleanup-Worker } | Out-Null + +$StartInfo = New-Object System.Diagnostics.ProcessStartInfo -Property @{ + FileName = $PhpPath + Arguments = $CommandArgs + UseShellExecute = $false + RedirectStandardOutput = $true + RedirectStandardError = $true +} + +# Function to read and log process output +function Read-ProcessOutput { + param ( + [System.Diagnostics.Process]$Process + ) + + while (!$Process.HasExited) { + while (!$Process.StandardOutput.EndOfStream) { + $Output = $Process.StandardOutput.ReadLine() + Log $Output + } + + while (!$Process.StandardError.EndOfStream) { + $Err = $Process.StandardError.ReadLine() + Log "ERROR: $Err" + } + + Start-Sleep -Milliseconds 100 + } + + # Read remaining output after the process exits + while (!$Process.StandardOutput.EndOfStream) { + $Output = $Process.StandardOutput.ReadLine() + Log $Output + } + while (!$Process.StandardError.EndOfStream) { + $Err = $Process.StandardError.ReadLine() + Log "ERROR: $Err" + } +} + +# Monitor loop +while ($true) { + # Create new process + $WorkerProcess = New-Object System.Diagnostics.Process + # Assign previously created StartInfo properties + $WorkerProcess.StartInfo = $StartInfo + + try { + # Start process + [void]$WorkerProcess.Start() + Write-Host "Process started." + + # Read and log process output + Read-ProcessOutput -Process $WorkerProcess + + } catch { + Write-Host "Error: $_" + } + + # Log the restart event + Log "Artisan queue worker stopped. Restarting in $RestartDelay seconds..." + # Delay before restarting + Start-Sleep -Seconds $RestartDelay +}