Cackledaemon.psm1
$CackledaemonWD = Join-Path $env:APPDATA 'cackledaemon' function New-CackledaemonWD { New-Item -Path $CackledaemonWD -ItemType directory } function Enable-CackledaemonJob { [CmdletBinding()] param( [Parameter(Position=0)] [string]$Name, [Parameter(Position=1)] [ScriptBlock]$ScriptBlock ) $Job = Get-Job -Name $Name -ErrorAction SilentlyContinue if ($Job) { Write-LogWarning ('{0} job already exists. Trying to stop and remove...' -f $Name) Disable-CackledaemonJob -ErrorAction Stop } $Job = Get-Job -Name $Name -ErrorAction SilentlyContinue if ($Job) { Write-LogError -Message ('{0} job somehow still exists - not attempting to start a new one.' -f $Name) ` -Category 'ResourceExists' ` -CategoryActivity 'Enable-CackledaemonJob' ` -CategoryReason 'UnstoppableJobException' } else { Start-Job ` -Name $Name ` -InitializationScript { Import-Module Cackledaemon } ` -ScriptBlock $ScriptBlock } } function Disable-CackledaemonJob { [CmdletBinding()] param( [Parameter(Position=0)] [string]$Name ) $Job = Get-Job -Name $Name -ErrorAction SilentlyContinue if (-not $Job) { Write-LogWarning ("{0} job doesn't exist. Doing nothing." -f $Name) } try { Stop-Job -Name $Name -ErrorAction Stop Remove-Job -Name $Name } catch { Write-LogError -Message ('Failed to stop and remove {0} job.' -f $Name) ` -Exception $_.Exception ` -Category $_.CategoryInfo.Category ` -CategoryActivity $_.CategoryInfo.Activity ` -CategoryReason $_.CategoryInfo.Reason ` -CategoryTargetName $_.CategoryInfo.TargetName ` -CategoryTargetType $_.CategoryInfo.TargetType } } $CackledaemonLogFile = Join-Path $CackledaemonWD 'Cackledaemon.log' $EmacsStdOutLogFile = Join-Path $CackledaemonWD 'EmacsStdout.log' $EmacsStdErrLogFile = Join-Path $CackledaemonWD 'EmacsStderr.log' $LogSize = 1mb $LogRotate = 4 $LogCheckTime = 2 # Seconds function Write-Log { param( [Parameter(Position=0)] [string]$Message, [string]$Level = 'Verbose', [string]$Exception, [string]$Category = 'NotSpecified', [string]$CategoryActivity, [string]$CategoryReason, [string]$CategoryTargetName, [string]$CategoryTargetType ) if (-not @('Debug', 'Verbose', 'Warning', 'Error').Contains($Level)) { Write-Warning ('Write-Log called with unrecognized level {0}' -f $Level) $Level = 'Warning' } if ( (-not @( 'NotSpecified', 'OpenError', 'CloseError', 'DeviceError', 'DeadlockDetected', 'InvalidArgument', 'InvalidData', 'InvalidOperation', 'OperationTimeout', 'SyntaxError', 'ParserError', 'PermissionDenied', 'ResourceBusy', 'ResourceExists', 'ResourceUnavailable', 'ReadError', 'WriteError', 'FromStdErr', 'SecurityError', 'ProtocolError', 'ConnectionError', 'AuthenticationError', 'LimitsExceeded', 'QuotaExceeded', 'NotEnabled' ).Contains($Category)) -and ($Level -eq 'Error') ) { Write-Warning ('Write-Log called with unrecognized error category "{0}"' -f $Category) $Category = 'NotSpecified' } $Line = ('[{0}] {1}: {2}' -f (Get-Date -Format o), $Level, $Message) Add-Content $CackledaemonLogFile -value $Line if ($Level -eq 'Debug') { Write-Debug $Message } elseif ($Level -eq 'Verbose') { Write-Verbose $Message } elseif ($Level -eq 'Warning') { Write-Warning $Message } elseif ($Level -eq 'Error') { if ($Exception) { $Message = ('{0} (Exception: {1})' -f $Message, $Exception) } Write-Error -Message $Message ` -Exception $Exception ` -Category $Category ` -CategoryActivity $CategoryActivity ` -CategoryReason $CategoryReason ` -CategoryTargetName $CategoryTargetName ` -CategoryTargetType $CategoryTargetType } } function Write-LogDebug { [Parameter(Position=0)] param([string]$Message) Write-Log $Message -Level Debug } function Write-LogVerbose { [Parameter(Position=0)] param([string]$Message) Write-Log $Message -Level Verbose } function Write-LogWarning { [Parameter(Position=0)] param([string]$Message) Write-Log $Message -Level Warning } function Write-LogError { [Parameter(Position=0)] param([string]$Message) [string]$Exception, [string]$Category = 'NotSpecified', [string]$CategoryActivity, [string]$CategoryReason, [string]$CategoryTargetName, [string]$CategoryTargetType Write-Log -Level Error ` -Message $Message ` -Exception $Exception ` -Category $Category ` -CategoryActivity $CategoryActivity ` -CategoryReason $CategoryReason ` -CategoryTargetName $CategoryTargetName ` -CategoryTargetType $CategoryTargetType } function Invoke-LogRotate { [CmdletBinding()] param() while ($true) { @($CackledaemonLogFile, $EmacsStdoutLogFile, $EmacsStdErrLogFile) | ForEach-Object { $LogFile = $_ if ((Get-Item $LogFile).Length -ge $LogSize) { Write-LogVerbose ('Rotating {0}...' -f $LogFile) ($LogRotate..0) | ForEach-Object { $Current = $(if ($_) { '{0}.{1}' -f $LogFile, $_ } else { $LogFile }) $Next = '{0}.{1}' -f $LogFile, ($_ + 1) if (Test-Path $Current) { Write-Log ('Copying {0} to {1}...' -f $Current, $Next) Copy-Item -Path $Current -Destination $Next } } Write-LogVerbose ('Truncating {0}...' -f $LogFile) Clear-Content $LogFile $StaleLogFile = '{0}.{1}' -f ($LogRotate + 1) if (Test-Path $StaleLogFile) { Write-LogVerbose ('Removing {0}...' -f $StaleLogFile) Remove-Item $StaleLogFile } Write-LogVerbose 'Done.' } } Write-LogDebug ('No need to rotate logs. Sleeping for {0} seconds.' -f $LogCheckTime) Start-Sleep -Seconds $LogCheckTime } } function Enable-LogRotateJob { [CmdletBinding()] param() Enable-CackledaemonJob 'LogRotateJob' { Invoke-LogRotate } } function Disable-LogRotateJob { [CmdletBinding()] param() Disable-CackledaemonJob 'LogRotateJob' } $PidFile = Join-Path $CackledaemonWD 'DaemonPidFile.json' function Write-EmacsProcessToPidFile { param([System.Diagnostics.Process]$Process) ($Process).Id | ConvertTo-Json | Out-File $PidFile } function Get-EmacsProcessFromPidFile { if (-not (Test-Path $PidFile)) { return $null } $Id = (Get-Content $PidFile | ConvertFrom-Json) if (-not $Id) { Remove-Item $PidFile return $null } return Get-Process -Id $Id -ErrorAction SilentlyContinue } function Get-UnmanagedEmacsDaemons () { $ManagedProcess = $(Get-EmacsProcessFromPidFile) return Get-CimInstance -Query " SELECT * FROM Win32_Process WHERE Name = 'emacs.exe' OR Name = 'runemacs.exe' " | Where-Object { $_.CommandLine.Contains("--daemon") } | ForEach-Object { Get-Process -Id ($_.ProcessId) } | Where-Object { -not ($_.Id -eq $ManagedProcess.Id) } } function Start-EmacsDaemon { [CmdletBinding()] param ([switch]$Wait) $Process = $(Get-EmacsProcessFromPidFile) if ($Process) { Write-LogError ` -Message 'The Emacs daemon is already running and being managed.' ` -Category ResourceExists ` -CategoryActivity 'Start-EmacsDaemon' ` -CategoryReason ManagedResourceExistsException } elseif ($(Get-UnmanagedEmacsDaemons)) { Write-LogError ` -Message 'An unmanaged Emacs daemon is running.' ` -Category ResourceExists ` -CategoryActivity 'Start-EmacsDaemon' ` -CategoryReason UnmanagedResourceExistsException } else { Write-LogVerbose 'Starting the Emacs daemon...' $Process = Start-Process ` -FilePath 'emacs.exe' ` -ArgumentList '--daemon' ` -NoNewWindow ` -RedirectStandardOut $EmacsStdOutLogFile ` -RedirectStandardError $EmacsStdErrLogFile ` -PassThru Write-EmacsProcessToPidFile $Process if ($Wait) { Write-Verbose 'Waiting for Emacs daemon to exit...' $Process = Wait-Process -InputObject $Process } Write-Verbose 'Done.' return $Process } } function Get-EmacsDaemon { [CmdletBinding()] param() Get-EmacsProcessFromPidFile } function Stop-EmacsDaemon { [CmdletBinding()] param() $Process = Get-EmacsProcessFromPidFile if (-not $Process) { Write-LogError ` -Message "A managed Emacs daemon isn't running and can not be stopped!" ` -Category ResourceUnavailable ` -CategoryActivity 'Stop-EmacsDaemon' ` -CategoryReason ManagedResourceUnavailableException } else { Write-LogVerbose 'Stopping the Emacs daemon...' Stop-Process -InputObject $Process Write-EmacsProcessToPidFile $null Write-LogVerbose 'Done.' } } Add-Type -AssemblyName System.Windows.Forms function Invoke-Applet { [CmdletBinding()] param() # The parent Form $Global:AppletForm = New-Object System.Windows.Forms.Form $AppletForm.Visible = $False $AppletForm.WindowState = "minimized" $AppletForm.ShowInTaskbar = $False # The NotifyIcon $Global:AppletIcon = New-Object System.Windows.Forms.NotifyIcon $AppletIcon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon( (Get-Command 'emacs.exe').Path ) $AppletIcon.Visible = $True $NotifyTimeout = 5000 # Notify the user if something fails function Start-InstrumentedBlock { param( [Parameter(Position=0)] [string]$Message, [Parameter(Position=1)] [ScriptBlock]$ScriptBlock, [System.Windows.Forms.ToolTipIcon]$Icon = [System.Windows.Forms.ToolTipIcon]::Warning ) try { Invoke-Command -ScriptBlock $ScriptBlock } catch { Write-LogError -Message $_.Exception.Message ` -Exception $_.Exception ` -Category $_.CategoryInfo.Category ` -CategoryActivity $_.CategoryInfo.Activity ` -CategoryReason $_.CategoryInfo.Reason ` -CategoryTargetName $_.CategoryInfo.TargetName ` -CategoryTargetType $_.CategoryInfo.TargetType $AppletIcon.BalloonTipIcon = $Icon $AppletIcon.BalloonTipTitle = $Message $AppletIcon.BalloonTipText = $_.Exception $AppletIcon.ShowBalloonTip($NotifyTimeout) } } # The right-click menu $ContextMenu = New-Object System.Windows.Forms.ContextMenu $AppletIcon.ContextMenu = $ContextMenu # Status items $DaemonStatusItem = New-Object System.Windows.Forms.MenuItem $DaemonStatusItem.Index = 0 $DaemonStatusItem.Text = '[???] Emacs Daemon' $ContextMenu.MenuItems.Add($DaemonStatusItem) | Out-Null $LogRotateStatusItem = New-Object System.Windows.Forms.MenuItem $LogRotateStatusItem.Text = '[???] Emacs Logs Rotation' $ContextMenu.MenuItems.Add($LogRotateStatusItem) | Out-Null $AppletIcon.add_MouseDown({ $Process = Get-EmacsProcessFromPidFile if ($Process) { $DaemonStatusItem.Text = '[RUNNING] Emacs Daemon' $StartDaemonItem.Enabled = $False $StopDaemonItem.Enabled = $True } else { $DaemonStatusItem.Text = '[STOPPED] Emacs Daemon' $StartDaemonItem.Enabled = $True $StopDaemonItem.Enabled = $False } $Job = Get-Job -Name 'LogRotateJob' -ErrorAction SilentlyContinue if ($Job) { $State = $Job.State.ToUpper() if ($State -eq 'RUNNING') { $State = 'ENABLED' } $LogRotateStatusItem.Text = ('[{0}] Logs Rotation' -f $State) $EnableLogRotateJobItem.Enabled = $False $DisableLogRotateJobItem.Enabled = $True } else { $LogRotateStatusItem.Text = '[DISABLED] Logs Rotation' $EnableLogRotateJobItem.Enabled = $True $DisableLogRotateJobItem.Enabled = $False } }) $ContextMenu.MenuItems.Add('-') | Out-Null # Daemon lifecycle items $StartDaemonItem = New-Object System.Windows.Forms.MenuItem $StartDaemonItem.Text = 'Start Emacs Daemon...' $StartDaemonItem.add_Click({ Start-InstrumentedBlock 'Failed to start the Emacs daemon' { Start-EmacsDaemon -ErrorAction Stop } }) $ContextMenu.MenuItems.Add($StartDaemonItem) | Out-Null $StopDaemonItem = New-Object System.Windows.Forms.MenuItem $StopDaemonItem.Text = 'Stop Emacs Daemon...' $StopDaemonItem.add_Click({ Start-InstrumentedBlock 'Failed to stop the Emacs daemon' { Stop-EmacsDaemon -ErrorAction Stop } }) $ContextMenu.MenuItems.Add($StopDaemonItem) | Out-Null $RestartDaemonItem = New-Object System.Windows.Forms.MenuItem $RestartDaemonItem.Text = 'Restart Emacs Daemon...' $RestartDaemonItem.add_Click({ Start-InstrumentedBlock 'Failed to restart the Emacs daemon' { Restart-EmacsDaemon -ErrorAction Stop } }) $ContextMenu.MenuItems.Add($RestartDaemonItem) | Out-Null $ContextMenu.MenuItems.Add('-') | Out-Null # Log rotate items $EnableLogRotateJobItem = New-Object System.Windows.Forms.MenuItem $EnableLogRotateJobItem.Text = 'Enable Log Rotation...' $EnableLogRotateJobItem.add_Click({ Start-InstrumentedBlock 'Failed to enable log rotation' { Enable-LogRotateJob -ErrorAction Stop } }) $ContextMenu.MenuItems.Add($EnableLogRotateJobItem) | Out-Null $DisableLogRotateJobItem = New-Object System.Windows.Forms.MenuItem $DisableLogRotateJobItem.Text = 'Disable Log Rotation...' $DisableLogRotateJobItem.add_Click({ Start-InstrumentedBlock 'Failed to disable log rotation' { Disable-LogRotateJob -ErrorAction Stop } }) $ContextMenu.MenuItems.Add($DisableLogRotateJobItem) | Out-Null $ContextMenu.MenuItems.Add('-') | Out-Null $OpenWDItem = New-Object System.Windows.Forms.MenuItem $OpenWDItem.Text = 'Open Working Directory...' $OpenWDItem.add_Click({ Start-InstrumentedBlock 'Failed to open working directory' { Start-Process $CackledaemonWD -ErrorAction Stop } }) $ContextMenu.MenuItems.Add($OpenWDItem) | Out-Null $ContextMenu.MenuItems.Add('-') | Out-Null $ExitItem = New-Object System.Windows.Forms.MenuItem $ExitItem.Text = 'Exit' $ContextMenu.MenuItems.Add($ExitItem) | Out-Null # Lifecycle events $AppletForm.add_Load({ Start-InstrumentedBlock 'Failed to initialize Cackledaemon' { Start-EmacsDaemon -ErrorAction Stop Enable-LogRotateJob -ErrorAction Stop } }) $ExitItem.add_Click({ Start-InstrumentedBlock 'Failed to gracefully shut down Cackledaemon' { Stop-EmacsDaemon -ErrorAction Stop Disable-LogRotateJob -ErrorAction Stop } $AppletIcon.Visible = $False $AppletIcon.Dispose() $AppletForm.Close() Remove-Variable -Name AppletForm -Scope Global Remove-Variable -Name AppletIcon -Scope Global }) $AppletForm.ShowDialog() | Out-Null } Export-ModuleMember ` -Function @( 'Invoke-LogRotate', 'Enable-LogRotateJob', 'Disable-LogRotateJob', 'Start-EmacsDaemon', 'Get-EmacsDaemon', 'Stop-EmacsDaemon', 'Restart-EmacsDaemon', 'Get-UnmanagedEmacsDaemons', 'Invoke-Applet' ) ` -Variable @( 'CackledaemonWD' ) # Copyright 2020 Josh Holbrook # # This file is part of Cackledaemon. # # Cackledaemon is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Cackledaemon is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Cackledaemon. if not, see <https://www.gnu.org/licenses/>. |