Cackledaemon.psm1
# Copyright 2020 Josh Holbrook # # This file is part of Cackledaemon and 100% definitely not a part of Emacs. # # 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/>. Set-Alias Invoke-CDInstallWizard (Join-Path $PSScriptRoot 'InstallWizard.ps1') $CackledaemonWD = Join-Path $Env:AppData 'Cackledaemon' $CackledaemonConfigLocation = Join-Path $CackledaemonWD 'Configuration.ps1' function New-CackledaemonWD { param( [switch]$NoShortcuts ) New-Item -Path $CackledaemonWD -ItemType directory $ModuleDirectory = Split-Path -Path (Get-Module Cackledaemon).Path -Parent Copy-Item (Join-Path $ModuleDirectory 'Configuration.ps1') (Join-Path $CackledaemonWD 'Configuration.ps1') if (-not $NoShortcuts) { Copy-Item (Join-Path $ModuleDirectory 'Shortcuts.csv') (Join-Path $CackledaemonWD 'Shortcuts.csv') } } class ShortcutCsvRecord { [string]$ShortcutName [string]$EmacsBinaryName [string]$ArgumentList [string]$Description ShortcutCsvRecord( [string]$ShortcutName, [string]$EmacsBinaryName, [string]$ArgumentList, [string]$Description ) { $this.ShortcutName = $ShortcutName $this.EmacsBinaryName = $EmacsBinaryName $this.ArgumentList = $ArgumentList $this.Description = $Description } } class ShortcutRecord { [string]$ShortcutName [string]$EmacsBinaryName [string[]]$ArgumentList [string]$Description ShortcutRecord( [string]$ShortcutName, [string]$EmacsBinaryName, [string[]]$ArgumentList, [string]$Description ) { $this.ShortcutName = $ShortcutName $this.EmacsBinaryName = $EmacsBinaryName $this.ArgumentList = $ArgumentList $this.Description = $Description } } function Get-ShortcutsConfig { Import-Csv -Path (Join-Path $CackledaemonWD './Shortcuts.csv') | ForEach-Object { New-Object ShortcutRecord $_.ShortcutName, $_.EmacsBinaryName, ($_.ArgumentList | ConvertFrom-Json), $_.Description } } function Enable-Job { [CmdletBinding()] param( [Parameter(Position=0)] [string]$Name, [Parameter(Position=1)] [ScriptBlock]$ScriptBlock ) $Job = Get-Job -Name $Name -ErrorAction SilentlyContinue if ($Job) { Write-CDWarning ('{0} job already exists. Trying to stop and remove...' -f $Name) Disable-Job -Name $Job.Name -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-Job' ` -CategoryReason 'UnstoppableJobException' } else { Start-Job ` -Name $Name ` -InitializationScript { Import-Module Cackledaemon } ` -ScriptBlock $ScriptBlock } } function Disable-Job { [CmdletBinding()] param( [Parameter(Position=0)] [string]$Name ) $Job = Get-Job -Name $Name -ErrorAction SilentlyContinue if (-not $Job) { Write-CDWarning ("{0} job doesn't exist. Doing nothing." -f $Name) return } try { Stop-Job -Name $Name -ErrorAction Stop Remove-Job -Name $Name -ErrorAction Stop } catch { Write-CDError $_ } } function New-CDLogRecord { [CmdletBinding()] param( [string]$Level = 'Info', [object]$MessageData, [string[]]$Tags = @() ) if (-not @('Debug','Info','Warning','Error','Fatal').Contains($Level)) { Write-Warning "New-CDLogRecord called with unrecognized level $Level" $Level = 'Warning' } $WriteInformation = Get-Command 'Write-Information' -CommandType Cmdlet & $WriteInformation $MessageData (@($Level) + $Tags) 6>&1 } function Write-CDLog { [CmdletBinding()] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [System.Management.Automation.InformationRecord]$InformationRecord ) try { . $CackledaemonConfigLocation } catch { Write-Warning 'Unable to load Cackledaemon configuration! Unable to write to log file.' return } if ($InformationRecord) { $Timestamp = (Get-Date -Date $InformationRecord.TimeGenerated -Format o) $InformationRecord.MessageData | Out-String | ForEach-Object { if ($_) { $Line = '{0} [{1}] {2}' -f $Timestamp,($InformationRecord.Tags -join ':'),$_ Add-Content $CackledaemonLogFile -Value $Line } } } } function Write-CDDebug { [CmdletBinding()] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [string]$Message, [string[]]$Tags = @() ) New-CDLogRecord 'Debug' $Message $Tags | Write-CDLog Write-Debug $Message } function Write-CDInfo { [CmdletBinding()] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [object]$MessageData, [string[]]$Tags = @() ) New-CDLogRecord 'Info' $MessageData $Tags | Write-CDLog Write-Information $MessageData $Tags } function Write-CDWarning { [CmdletBinding()] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [string]$Message, [string[]]$Tags = @() ) New-CDLogRecord 'Warning' $Message $Tags | Write-CDLog Write-Warning $Message } function Write-CDError { [CmdletBinding()] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [System.Management.Automation.ErrorRecord]$ErrorRecord, [string[]]$Tags = @() ) New-CDLogRecord 'Error' $ErrorRecord $Tags | Write-CDLog $PSCmdlet.WriteError($ErrorRecord) } function Write-CDFatal { [CmdletBinding()] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [System.Management.Automation.ErrorRecord]$ErrorRecord, [string[]]$Tags = @() ) New-CDLogRecord 'Fatal' $ErrorRecord $Tags | Write-CDLog $PSCmdlet.ThrowTerminatingError($ErrorRecord) } function Invoke-LogRotate { [CmdletBinding()] param() . $CackledaemonConfigLocation @($CackledaemonLogFile, $EmacsStdoutLogFile, $EmacsStdErrLogFile) | ForEach-Object { $LogFile = $_ if ((Test-Path $LogFile) -and (Get-Item $LogFile).Length -ge $LogSize) { Write-CDInfo ('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-CDInfo ('Copying {0} to {1}...' -f $Current, $Next) Copy-Item -Path $Current -Destination $Next } } Write-CDInfo ('Truncating {0}...' -f $LogFile) Clear-Content $LogFile $StaleLogFile = '{0}.{1}' -f $LogFile, ($LogRotate + 1) if (Test-Path $StaleLogFile) { Write-CDInfo ('Removing {0}...' -f $StaleLogFile) Remove-Item $StaleLogFile } Write-CDInfo 'Done.' } } } function Enable-CDLogRotateJob { [CmdletBinding()] param() Enable-Job 'CDLogRotateJob' { . $CackledaemonConfigLocation while ($True) { Invoke-LogRotate Write-CDDebug ('CDLogRotateJob sleeping for {0} seconds.' -f $LogCheckTime) Start-Sleep -Seconds $LogCheckTime } } } function Disable-CDLogRotateJob { [CmdletBinding()] param() Disable-Job 'CDLogRotateJob' } function Test-EmacsExe { . $CackledaemonConfigLocation Test-Path (Join-Path $EmacsInstallLocation 'bin\emacs.exe') } class Version : IComparable { [int]$Major [int]$Minor Version([int64]$Major, [int64]$Minor) { $this.Major = $Major $this.Minor = $Minor } [int]CompareTo([object]$Other) { if ($Other -eq $null) { return 1 } $Other = [Version]$Other if ($this.Major -gt $Other.Major) { return 1 } elseif ($this.Major -lt $Other.Major) { return -1 } elseif ($this.Minor -gt $Other.Minor) { return 1 } elseif ($this.Minor -lt $Other.Minor) { return -1 } else { return 0 } } [string]ToString() { return 'v{0}.{1}' -f $this.Major, $this.Minor } } function New-Version { param( [int]$Major, [int]$Minor ) return New-Object Version $Major, $Minor } function Get-EmacsExeVersion { if (Test-EmacsExe) { . $CackledaemonConfigLocation $EmacsExe = Join-Path $EmacsInstallLocation 'bin\emacs.exe' if ((& $EmacsExe --version)[0] -match '^GNU Emacs (\d+)\.(\d+)$') { New-Version $Matches[1] $Matches[2] } } } class Download : IComparable { [Version]$Version [string]$Href Download([int64]$Major, [int64]$Minor, [string]$Href) { $this.Version = New-Object Version $Major, $Minor $this.Href = $Href } [int]CompareTo([object]$Other) { if ($Other -eq $null) { return 1 } $Other = [Download]$Other return $this.Version.CompareTo($Other.Version) } [string]ToString() { return 'Download($Version={0}; $Href={1})' -f $this.Version, $this.Href } } function New-Download { param( [int]$Major, [int]$Minor, [string]$Href ) New-Object Download $Major, $Minor, $Href } function Get-EmacsDownload { . $CackledaemonConfigLocation return (Invoke-WebRequest $EmacsDownloadsEndpoint).Links | ForEach-Object { if ($_.href -match '^emacs-(\d+)/$') { $MajorPathPart = $_.href if ([int]$Matches[1] -lt 25) { return } (Invoke-WebRequest ($EmacsDownloadsEndpoint + $MajorPathPart)).Links | ForEach-Object { if ($_.href -match '^emacs-(\d+)\.(\d+)-x86_64\.zip$') { $Href = $EmacsDownloadsEndpoint + $MajorPathPart + $_.href return New-Download $Matches[1] $Matches[2] $Href } } } } | Where-Object {$_} } function Get-LatestEmacsDownload { (Get-EmacsDownload | Measure-Object -Maximum).Maximum } class Workspace { [System.IO.DirectoryInfo]$Root [System.IO.DirectoryInfo]$Archives [System.IO.DirectoryInfo]$Installs [System.IO.DirectoryInfo]$Backups Workspace([string]$Path) { $ArchivesPath = Join-Path $Path 'Archives' $InstallsPath = Join-Path $Path 'Installs' $BackupsPath = Join-Path $Path 'Backups' $this.Root = Get-Item $Path $this.Archives = Get-Item $ArchivesPath $this.Installs = Get-Item $InstallsPath $this.Backups = Get-Item $BackupsPath } [string]GetKey([Version]$Version) { return 'emacs-{0}.{1}-x86_64' -f $Version.Major, $Version.Minor } [string]GetArchivePath([Version]$Version) { return Join-Path $this.Archives ('{0}.zip' -f $this.GetKey($Version)) } [boolean]TestArchive([Version]$Version) { return Test-Path $this.GetArchivePath($Version) } [System.IO.FileInfo]GetArchive([Version]$Version) { return Get-Item $this.GetArchivePath($Version) } [string]GetInstallPath([Version]$Version) { return Join-Path $this.Installs $this.GetKey($Version) } [boolean]TestInstall([Version]$Version) { return Test-Path $this.GetInstallPath($Version) } [System.IO.DirectoryInfo]GetInstall([Version]$Version) { return Get-Item $this.GetInstallPath($Version) } } function Test-Workspace { . $CackledaemonConfigLocation Test-Path $WorkspaceDirectory } function Get-Workspace { . $CackledaemonConfigLocation return New-Object Workspace $WorkspaceDirectory } function New-Workspace { . $CackledaemonConfigLocation $ArchivesPath = Join-Path $WorkspaceDirectory 'Archives' $InstallsPath = Join-Path $WorkspaceDirectory 'Installs' $BackupsPath = Join-Path $WorkspaceDirectory 'Backups' New-Item -Type Directory $WorkspaceDirectory | Out-Null New-Item -Type Directory $ArchivesPath | Out-Null New-Item -Type Directory $InstallsPath | Out-Null New-Item -Type Directory $BackupsPath | Out-Null return New-Object Workspace $WorkspaceDirectory } function New-EmacsArchive { param( [Parameter(Position=0)] [Download]$Download ) $Workspace = Get-Workspace $Archive = $Workspace.GetArchivePath($Download.Version) Invoke-WebRequest ` -Uri $Download.Href ` -OutFile $Archive | Out-Null return Get-Item $Archive } function Export-EmacsArchive { param( [Parameter(Position=0)] [string]$Path ) $Workspace = Get-Workspace $Key = [IO.Path]::GetFileNameWithoutExtension($Path) $Destination = Join-Path $Workspace.Installs.FullName $Key Expand-Archive -Path $Path -DestinationPath $Destination return Get-Item $Destination } function Update-EmacsInstall { param( [string]$Path ) $Source = Get-Item -ErrorAction Stop $Path . $CackledaemonConfigLocation $Workspace = Get-Workspace $Backup = Join-Path $Workspace.Backups ('emacs-{0}' -f (Get-Date -Format 'yyyyMMddHHmmss')) if (Test-Path $EmacsInstallLocation -ErrorAction Stop) { Copy-Item $EmacsInstallLocation $Backup -ErrorAction Stop Remove-Item -Recurse $EmacsInstallLocation -ErrorAction Stop } Move-Item $Source $EmacsInstallLocation -ErrorAction Stop Remove-Item -Recurse $Backup -ErrorAction SilentlyContinue return Get-Item $EmacsInstallLocation } function Set-EmacsPathEnvVariable { [CmdletBinding()] param() . $CackledaemonConfigLocation $Path = Join-Path $EmacsInstallLocation 'bin' $ExistingEmacs = Get-Command 'emacs.exe' -ErrorAction SilentlyContinue if ($ExistingEmacs) { $ExistingEmacsBinDir = Split-Path $ExistingEmacs.Source -Parent } if ($ExistingEmacs -and -not ($ExistingEmacsBinDir -eq $Path)) { Write-CDWarning ('An unmanaged Emacs is already installed at {0} - this may cause unexpected behavior.' -f $ExistingEmacsBinDir) } $PathProperty = (Get-ItemProperty -Path 'HKCU:\Environment' -Name 'Path') $PathParts = $PathProperty.Path.Split(';') | Where-Object { $_ } $ExistingEmacsPathPart = $PathParts | Where-Object { $_ -eq $Path } if ($ExistingEmacsPathPart) { Write-CDInfo 'Emacs is already in the PATH - no changes necessary.' } else { $PathProperty.Path += ($Path + ';') Set-ItemProperty -Path 'HKCU:\Environment' -Name 'Path' -Value $PathProperty } } function Set-HomeEnvVariable { . $CackledaemonConfigLocation Set-ItemProperty -Path 'HKCU:\Environment' -Name 'HOME' -Value $HomeDirectory } function Set-EmacsAppPathRegistryKeys { . $CackledaemonConfigLocation @('emacs.exe', 'runemacs.exe', 'emacsclient.exe', 'emacsclientw.exe') | ForEach-Object { $RegistryPath = Join-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths' $_ $BinPath = Join-Path $EmacsInstallLocation "bin\$_" if (Test-Path $BinPath) { if (Test-Path -Path $RegistryPath) { Set-Item -Path $RegistryPath -Value $BinPath } else { New-Item -Path $RegistryPath -Value $BinPath } Set-ItemProperty -Path $RegistryPath -Name Path -Value $Path } else { $Exception = New-Object Exception ("{0} doesn't exist - refusing to write this to the registry." -f $BinPath) $ErrorRecord = New-Object System.Management.Automation.ErrorRecord $Exception,'ItemNotFoundException','ObjectNotFound',$BinPath Write-CDError $ErrorRecord } } } function Get-StartMenuItems { . $CackledaemonConfigLocation Get-ChildItem -Path $StartMenuPath -ErrorAction SilentlyContinue | ForEach-Object { Get-Item $_.FullName } } function Get-WShell { if (-not $WShell) { $Global:WShell = New-Object -comObject WScript.Shell } return $WShell } function Set-Shortcut { param( [string]$ShortcutPath, [string]$TargetPath, [string[]]$ArgumentList = @(), [string]$WorkingDirectory = $Env:UserProfile, [string]$Description ) $Shell = Get-WShell $Arguments = ($ArgumentList | ForEach-Object { if ($_ -match '[" ]') { return ('"{0}"' -f ($_ -replace '"', '\"')) } else { return ($_ -replace '([,;=\W])', '^$1') } }) -join ' ' $Shortcut = $Shell.CreateShortcut($ShortcutPath) $Shortcut.TargetPath = $TargetPath $Shortcut.Arguments = $Arguments $Shortcut.WorkingDirectory = $WorkingDirectory if ($Description) { $Shortcut.Description = $Description } $Shortcut.Save() } function Install-CDShortcuts { . $CackledaemonConfigLocation $Config = Get-ShortcutsConfig $CurrentItems = Get-StartMenuItems $DesiredShortcutPaths = $Config | ForEach-Object { Join-Path $StartMenuPath ($_.ShortcutName + ".lnk") } $CurrentItems | Where-Object { -not $DesiredShortcutPaths.Contains($_.FullName) } | ForEach-Object { Remove-Item $_ } $Config | ForEach-Object { Set-Shortcut ` -ShortcutPath (Join-Path $StartMenuPath ($_.ShortcutName + ".lnk")) ` -TargetPath (Join-Path "$EmacsInstallLocation\bin" $_.EmacsBinaryName) ` -ArgumentList $_.ArgumentList ` -Description $_.Description } } function Get-OpenEmacsProcesses { . $CackledaemonConfigLocation # This escape isn't thoroughly researched and may be brittle $EmacsSearchQuery = "${EmacsInstallLocation}%" -replace '\\', '\\' return Get-CimInstance -Query " SELECT * FROM Win32_Process WHERE ExecutablePath LIKE '${EmacsSearchQuery}' " | ForEach-Object { Get-Process -Id $_.ProcessId } } function Wait-ForEmacsProcessExit { [CmdletBinding()] param( [int]$PollingInterval = 5, # 1 Second [int]$Timeout = 60 # 1 minute ) $StartTime = Get-Date $OpenProcesses = Get-OpenEmacsProcesses while ($OpenProcesses) { if (((Get-Date) - $StartTime).TotalSeconds -gt $Timeout) { $Exception = New-Object Exception "Emacs processes are still open after $Timeout seconds!" $ErrorRecord = New-Object System.Management.Automation.ErrorRecord $Exception,'OpenEmacsProcessesException','LimitsExceeded',$null Write-CDFatal $ErrorRecord } Write-CDInfo "The following Emacs processes are open:" $OpenProcesses | Out-String | Write-CDInfo Write-CDInfo "Please close these processes to continue." Write-CDInfo "" Start-Sleep -Seconds $PollingInterval $OpenProcesses = Get-OpenEmacsProcesses } } function Invoke-PostInstallHook { . $CackledaemonConfigLocation if ($PostInstallHook -is [string]) { Invoke-Expression $PostInstallHook } if ($PostInstallHook -is [scriptblock]) { & $PostInstallHook } } function Install-Emacs { param( [switch]$Force ) $ErrorActionPreference = 'Stop' Write-Progress -Id 1 -Activity 'Installing Emacs' -CurrentOperation 'Loading Cackledaemon configuration...' -PercentComplete 0 Write-CDInfo 'Loading Cackledaemon configuration...' . $CackledaemonConfigLocation if (Test-Workspace) { $Workspace = Get-Workspace } else { Write-Progress -Id 1 -Activity 'Installing Emacs' -CurrentOperation 'Creating workspace...' -PercentComplete 09 Write-CDInfo 'Creating new workspace...' $Workspace = New-Workspace } Write-Progress -Id 1 -Activity 'Installing Emacs' -CurrentOperation 'Checking the Emacs website for the latest available download...' -PercentComplete 18 Write-CDInfo 'Checking the Emacs website for the latest available download...' $LatestDownload = Get-LatestEmacsDownload Write-CDInfo ('Version {0} is the latest version of Emacs available for install' -f $LatestDownload.Version) $ShouldInstall = $False Write-Progress -Id 1 -Activity 'Installing Emacs' -CurrentOperation 'Checking if Emacs needs to be installed or updated...' -PercentComplete 27 if ($Force) { Write-CDInfo "`-Force switch was enabled so installing Emacs regardless of what's installed" $ShouldInstall = $True } else { Write-Progress -ParentId 1 -Id 2 -Activity 'Checking the currently installed Emacs version' -CurrentOperation "Looking for an Emacs install in $EmacsInstallLocation..." -PercentComplete 0 if (Test-EmacsExe) { Write-Progress -ParentId 1 -Id 2 -Activity 'Checking the currently installed Emacs version' -CurrentOperation 'Running "Emacs --version"...' -PercentComplete 33 $InstalledVersion = Get-EmacsExeVersion Write-CDInfo ('Version {0} of Emacs is installed' -f $InstalledVersion) Write-Progress -ParentId 1 -Id 2 -Activity 'Checking the currently installed Emacs version' -CurrentOperation 'Comparing versions...' -PercentComplete 67 if ($LatestDownload.Version -gt $InstalledVersion) { Write-CDInfo ('Upstream Emacs version {0} is newer than installed Emacs version {1}' -f $LatestDownload.Version, $InstalledVersion) $ShouldInstall = $True } else { Write-CDInfo ('Upstream Emacs version {0} is no newer than installed Emacs version {1}' -f $LatestDownload.Version, $InstalledVersion) } } else { Write-CDInfo 'No version of Emacs is installed' $ShouldInstall = $True } Write-Progress -ParentId 1 -Id 2 -Activity 'Checking the currently installed Emacs version' -Completed } if (-not $ShouldInstall) { Write-Progress -Id 1 -Activity 'Installing Emacs' -Completed Write-CDInfo 'Emacs is currently installed and at the latest available version.' } else { $TargetVersion = $LatestDownload.Version if ($Workspace.TestInstall($TargetVersion)) { Write-CDInfo "Emacs has already been downloaded and unpacked for version $TargetVersion" $Install = $Workspace.GetInstall($TargetVersion) } else { if ($Workspace.TestArchive($TargetVersion)) { Write-CDInfo "Emacs has already been downloaded (but not unpacked) for version $TargetVersion" $Archive = $Workspace.GetArchive($TargetVersion); } else { Write-Progress -Id 1 -Activity 'Installing Emacs' -CurrentOperation "Downloading Emacs version $TargetVersion..." -PercentComplete 36 Write-CDInfo "Downloading Emacs version $TargetVersion..." $Archive = New-EmacsArchive $LatestDownload } Write-Progress -Id 1 -Activity 'Installing Emacs' -CurrentOperation "Unpacking Emacs version $TargetVersion..." -PercentComplete 45 Write-CDInfo "Unpacking Emacs version $TargetVersion..." $Install = Export-EmacsArchive $Archive } Write-Progress -Id 1 -Activity 'Installing Emacs' -CurrentOperation "Waiting for Emacs to exit..." Write-CDInfo 'In order to install Emacs, all Emacs processes must be closed.' Wait-ForEmacsProcessExit Write-Progress -Id 1 -Activity 'Installing Emacs' -CurrentOperation 'Elevating privileges...' -PercentComplete 55 Write-CDInfo "Elevating privileges..." $ModulePath = Join-Path $PSScriptRoot 'Cackledaemon.psd1' Invoke-AsAdministrator " `$ErrorActionPreference = 'Stop' Import-Module $ModulePath Write-Progress -Id 1 -Activity 'Installing Emacs' -CurrentOperation 'Updating Emacs...' -PercentComplete 63 Write-CDInfo 'Updating Emacs...' Update-EmacsInstall -Path $Install | Out-Null Write-Progress -Id 1 -Activity 'Installing Emacs' -CurrentOperation 'Setting app path registry keys...' -PercentComplete 73 Write-CDInfo 'Setting app path registry keys...' Set-EmacsAppPathRegistryKeys | Out-Null Write-CDInfo 'Emacs $TargetVersion is installed and ready to rock!' " Write-Progress -Id 1 -Activity 'Installing Emacs' -CurrentOperation 'Running post-install hook...' -PercentComplete 91 Write-CDInfo "Running post-install hook..." Invoke-PostInstallHook Write-Progress -Id 1 -Activity 'Installing Emacs' -Completed } } function Install-EmacsUserEnvironment { $ErrorActionPreference = 'Stop' Write-Progress -Id 3 -Activity 'Setting up the Emacs user environment' -CurrentOperation "Updating the user's `$Path variable..." -PercentComplete 0 Write-CDInfo "Updating the user's `$Path variable..." Set-EmacsPathEnvVariable Write-Progress -Id 3 -Activity 'Setting up the Emacs user environment' -CurrentOperation "Setting the user's `$HOME variable..." -PercentComplete 33 Write-CDInfo "Setting the user's `$HOME variable..." Set-HomeEnvVariable Write-Progress -Id 3 -Activity 'Setting up the Emacs user environment' -CurrentOperation "Installing shortcuts..." -PercentComplete 67 Write-CDInfo "Installing shortcuts..." Install-CDShortcuts Write-Progress -Id 3 -Activity 'Setting up the Emacs user environment' -Completed } function Write-ProcessToPidFile { param([System.Diagnostics.Process]$Process) . $CackledaemonConfigLocation ($Process).Id | ConvertTo-Json | Out-File $PidFile } function Get-ProcessFromPidFile { . $CackledaemonConfigLocation if (-not (Test-Path $PidFile)) { return $null } $Id = (Get-Content $PidFile | ConvertFrom-Json) if ($Id) { $Process = Get-Process -Id $Id -ErrorAction SilentlyContinue } if (-not $Process) { Remove-Item $PidFile } return $Process } function Get-UnmanagedEmacsDaemon { $ManagedProcess = Get-ProcessFromPidFile 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) . $CackledaemonConfigLocation $Process = Get-ProcessFromPidFile if ($Process) { $Exception = New-Object Exception 'The Emacs daemon is already running and being managed.' $ErrorRecord = New-Object System.Management.Automation.ErrorRecord $Exception,'ManagedResourceExistsException','ResourceExists',$null Write-CDError $ErrorRecord } elseif (Get-UnmanagedEmacsDaemon) { $Exception = New-Object Exception 'An unmanaged Emacs daemon is running.' $ErrorRecord = New-Object System.Management.Automation.ErrorRecord $Exception,'UnmanagedResourceExistsException','ResourceExists',$null Write-CDError $ErrorRecord } else { Write-CDInfo 'Starting the Emacs daemon...' $Process = Start-Process ` -FilePath 'emacs.exe' ` -ArgumentList '--daemon' ` -NoNewWindow ` -RedirectStandardOut $EmacsStdOutLogFile ` -RedirectStandardError $EmacsStdErrLogFile ` -PassThru Write-ProcessToPidFile $Process if ($Wait) { Write-CDDebug 'Waiting for Emacs daemon to exit...' $Process = Wait-Process -Id $Process.Id } Write-CDInfo 'Done.' return $Process } } function Get-EmacsDaemon { [CmdletBinding()] param() Get-ProcessFromPidFile } function Stop-EmacsDaemon { [CmdletBinding()] param() $Process = Get-ProcessFromPidFile if (-not $Process) { $Exception = New-Object Exception "A managed Emacs daemon isn't running and can not be stopped." $ErrorRecord = New-Object System.Management.Automation.ErrorRecord $Exception,'UnmanagedResourceUnavailableException','ResourceUnavailable',$null Write-CDError $ErrorRecord } else { Write-CDInfo 'Stopping the Emacs daemon...' Stop-Process -InputObject $Process Write-ProcessToPidFile $null Write-CDInfo 'Done.' } } function Restart-EmacsDaemon { [CmdletBinding()] param() try { Stop-EmacsDaemon -ErrorAction Stop } catch { Write-CDWarning 'Attempting to start the Emacs daemon even though stopping it failed' } Start-EmacsDaemon } Add-Type -AssemblyName System.Windows.Forms function Invoke-CDApplet { [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 function Start-InstrumentedBlock { param( [string]$Message, [ScriptBlock]$ScriptBlock, [System.Windows.Forms.ToolTipIcon]$Icon = [System.Windows.Forms.ToolTipIcon]::Warning ) try { Invoke-Command -ScriptBlock $ScriptBlock } catch { try { . $CackledaemonConfigLocation } catch { Write-Warning 'Unable to load configuration! Using default notify timeout.' $NotifyTimeout = 5000 } Write-CDError $_ $AppletIcon.BalloonTipIcon = $Icon $AppletIcon.BalloonTipTitle = $Message $AppletIcon.BalloonTipText = $_.Exception $AppletIcon.ShowBalloonTip($NotifyTimeout) } } $ContextMenu = New-Object System.Windows.Forms.ContextMenu $AppletIcon.ContextMenu = $ContextMenu $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 $OnIconClick = { $Process = Get-ProcessFromPidFile if ($Process) { $DaemonStatusItem.Text = '[RUNNING] Emacs Daemon' $StartDaemonItem.Enabled = $False $StopDaemonItem.Enabled = $True $RestartDaemonItem.Enabled = $True } else { $DaemonStatusItem.Text = '[STOPPED] Emacs Daemon' $StartDaemonItem.Enabled = $True $StopDaemonItem.Enabled = $False $RestartDaemonItem.Enabled = $True } $Job = Get-Job -Name 'CDLogRotateJob' -ErrorAction SilentlyContinue if ($Job) { $State = $Job.State.ToUpper() if ($State -eq 'RUNNING') { $State = 'ENABLED' } $LogRotateStatusItem.Text = ('[{0}] Logs Rotation' -f $State) $EnableLogRotateItem.Enabled = $False $DisableLogRotateItem.Enabled = $True } else { $LogRotateStatusItem.Text = '[DISABLED] Logs Rotation' $EnableLogRotateItem.Enabled = $True $DisableLogRotateItem.Enabled = $False } } $AppletIcon.add_MouseDown($OnIconClick) $ContextMenu.MenuItems.Add('-') | Out-Null $StartDaemonItem = New-Object System.Windows.Forms.MenuItem $StartDaemonItem.Text = 'Start Emacs Daemon...' $OnStartDaemonClick = { Start-InstrumentedBlock 'Failed to start the Emacs daemon' { Start-EmacsDaemon -ErrorAction Stop } } $StartDaemonItem.add_Click($OnStartDaemonClick) $ContextMenu.MenuItems.Add($StartDaemonItem) | Out-Null $StopDaemonItem = New-Object System.Windows.Forms.MenuItem $StopDaemonItem.Text = 'Stop Emacs Daemon...' $StopDaemoClick = { Start-InstrumentedBlock 'Failed to stop the Emacs daemon' { Stop-EmacsDaemon -ErrorAction Stop } } $StopDaemonItem.add_Click($OnStopDaemonClick) $ContextMenu.MenuItems.Add($StopDaemonItem) | Out-Null $RestartDaemonItem = New-Object System.Windows.Forms.MenuItem $RestartDaemonItem.Text = 'Restart Emacs Daemon...' $OnRestartDaemonClick = { Start-InstrumentedBlock 'Failed to restart the Emacs daemon' { Restart-EmacsDaemon -ErrorAction Stop } } $RestartDaemonItem.add_Click($OnRestartDaemonClick) $ContextMenu.MenuItems.Add($RestartDaemonItem) | Out-Null $ContextMenu.MenuItems.Add('-') | Out-Null $EnableLogRotateItem = New-Object System.Windows.Forms.MenuItem $EnableLogRotateItem.Text = 'Enable Log Rotation...' $OnEnableLogRotateClick = { Start-InstrumentedBlock 'Failed to enable log rotation' { Enable-CDLogRotateJob -ErrorAction Stop } } $EnableLogRotateItem.add_Click($OnEnableLogRotateClick) $ContextMenu.MenuItems.Add($EnableLogRotateItem) | Out-Null $DisableLogRotateItem = New-Object System.Windows.Forms.MenuItem $DisableLogRotateItem.Text = 'Disable Log Rotation...' $OnDisableLogRotateClick = { Start-InstrumentedBlock 'Failed to disable log rotation' { Disable-CDLogRotateJob -ErrorAction Stop } } $DisableLogRotateItem.add_Click($OnDisableLogRotateClick) $ContextMenu.MenuItems.Add($DisableLogRotateItem) | Out-Null $ContextMenu.MenuItems.Add('-') | Out-Null $InstallWizardItem = New-Object System.Windows.Forms.MenuItem $InstallWizardItem.Text = 'Check for updates...' $OnInstallWizardClick = { Start-InstrumentedBlock 'Failed to launch install wizard' { Start-Process powershell.exe -ArgumentList @( '-NoExit', '-Command', (Join-Path $PSScriptRoot 'InstallWizard.ps1') ) } } $InstallWizardItem.add_Click($OnInstallWizardClick) $ContextMenu.MenuItems.Add($InstallWizardItem) | Out-Null $ContextMenu.MenuItems.Add('-') | Out-Null $EditConfigItem = New-Object System.Windows.Forms.MenuItem $EditConfigItem.Text = 'Edit Configuration...' $OnEditConfigClick = { Start-InstrumentedBlock 'Failed to edit configuration' { Start-Process $CackledaemonConfigLocation } } $EditConfigItem.add_Click($OnEditConfigClick) $ContextMenu.MenuItems.Add($EditConfigItem) | Out-Null $OpenWDItem = New-Object System.Windows.Forms.MenuItem $OpenWDItem.Text = 'Open Working Directory...' $OnOpenWdClick = { Start-InstrumentedBlock 'Failed to open working directory' { Start-Process $CackledaemonWD -ErrorAction Stop } } $OpenWDItem.add_Click($OnOpenWDClick) $ContextMenu.MenuItems.Add($OpenWDItem) | Out-Null $ContextMenu.MenuItems.Add('-') | Out-Null $OnLoad = { Start-InstrumentedBlock 'Failed to start the Emacs daemon' { Start-EmacsDaemon -ErrorAction Stop } Start-InstrumentedBlock 'Failed to enable log rotation' { Enable-CDLogRotateJob -ErrorAction Stop } } $AppletForm.add_Load($OnLoad) $ExitItem = New-Object System.Windows.Forms.MenuItem $ExitItem.Text = 'Exit' $ContextMenu.MenuItems.Add($ExitItem) | Out-Null $OnExit = { if (Get-EmacsDaemon) { Start-InstrumentedBlock 'Failed to gracefully shut down Emacs' { Stop-EmacsDaemon -ErrorAction Stop } } if (Get-Job -Name 'CDLogRotateJob' -ErrorAction SilentlyContinue) { Start-InstrumentedBlock 'Failed to gracefully shut down log rotation' { Disable-CDLogRotateJob -ErrorAction Stop } } $AppletIcon.Visible = $False $AppletIcon.Dispose() $AppletForm.Close() Remove-Variable -Name AppletForm -Scope Global Remove-Variable -Name AppletIcon -Scope Global } $ExitItem.add_Click($OnExit) $AppletForm.ShowDialog() | Out-Null } function Install-CDApplet { . $CackledaemonConfigLocation $StartupPath = Join-Path $Env:AppData 'Microsoft\Windows\Start Menu\Programs\Startup' @($StartMenuPath, $StartupPath) | ForEach-Object { Set-Shortcut ` -ShortcutPath (Join-Path $_ ("Cackledaemon.lnk")) ` -TargetPath (Join-Path $PSScriptRoot "Applet.vbs") ` -Description "Launch the Cackledaemon applet" } } $AliasesToExport = @('Invoke-CDInstallWizard') $FunctionsToExport = @( 'Clear-ServerFileDirectory', 'Disable-Job', 'Disable-CDLogRotateJob', 'Enable-Job', 'Enable-CDLogRotateJob', 'Export-EmacsArchive' 'Get-EmacsDaemon', 'Get-EmacsDownload', 'Get-EmacsExeVersion', 'Get-FileTypeAssociationsConfig', 'Get-LatestEmacsDownload', 'Get-OpenEmacsProcesses', 'Get-ProcessFromPidFile', 'Get-ShortcutsConfig', 'Get-StartMenuItems', 'Get-StartMenuPath', 'Get-UnmanagedEmacsDaemon', 'Get-WShell', 'Get-Workspace', 'Install-CDApplet', 'Install-Emacs', 'Install-EmacsUserEnvironment', 'Install-FileTypeAssociations', 'Install-CDShortcuts', 'Invoke-CDApplet', 'Invoke-LogRotate', 'Invoke-PostInstallHook', 'New-CackledaemonWD', 'New-CDLogRecord', 'New-Download', 'New-EmacsArchive', 'New-ServerFileDirectory', 'New-Shortcut', 'New-Version', 'New-Workspace', 'Restart-EmacsDaemon', 'Set-EmacsAppPathRegistryKeys', 'Set-EmacsPathEnvVariable', 'Set-HomeEnvVariable', 'Set-Shortcut', 'Start-EmacsDaemon', 'Stop-EmacsDaemon', 'Test-EmacsExe', 'Test-ServerFileDirectory', 'Update-EmacsInstall', 'Write-CDDebug', 'Write-CDError', 'Write-CDFatal', 'Write-CDInfo', 'Write-CDLog', 'Write-CDWarning', 'Write-ProcessToPidFile' ) $VariablesToExport = @( 'CackledaemonConfigLocation', 'CackledaemonWD' ) Export-ModuleMember ` -Alias $AliasesToExport ` -Function $FunctionsToExport ` -Variable $VariablesToExport |