Cackledaemon.psm1
$CackledaemonWD = Join-Path $Env:AppData 'Cackledaemon' $CackledaemonConfigLocation = Join-Path $CackledaemonWD 'Configuration.ps1' function New-CackledaemonWD { param( [switch]$NoShortcuts, [switch]$NoFileTypeAssociations ) 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') } if (-not $NoFileTypeAssociations) { Copy-Item (Join-Path $ModuleDirectory 'FileTypeAssociations.csv') (Join-Path $CackledaemonWD 'FileTypeAssociations.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 } } class FileTypeAssociationRecord { [string]$FileType [string]$Extension [string]$Command FileTypeAssociationRecord( [string]$FileType, [string]$Extension, [string]$Command ) { $this.FileType = $FileType $this.Extension = $Extension $this.Command = $Command } } class FileTypeAssociationCsvRecord : FileTypeAssociationRecord { FileTypeAssociationCsvRecord([string]$FileType, [string]$Extension, [string]$Command): base($FileType, $Extension, $Command) {} } function Get-FileTypeAssociationsConfig { Import-Csv -Path (Join-Path $CackledaemonWD './FileTypeAssociations.csv') | ForEach-Object { New-Object FileTypeAssociationRecord $_.FileType, $_.Extension, $_.Command } } 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-LogWarning ('{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-LogWarning ("{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-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 } } function Write-Log { param( [Parameter(Position=0)] [string]$Message, [string]$Level = 'Verbose', [Exception]$Exception, [System.Management.Automation.ErrorCategory]$Category = 'NotSpecified', [string]$CategoryActivity, [string]$CategoryReason, [string]$CategoryTargetName, [string]$CategoryTargetType ) Try { . $CackledaemonConfigLocation } Catch { Write-Warning 'Unable to load configuration! Unable to write to log file.' } if (-not @('Debug', 'Verbose', 'Information', 'Host', 'Warning', 'Error').Contains($Level)) { Write-LogWarning ('Write-Log called with unrecognized level {0}' -f $Level) $Level = 'Warning' } if ($Level -eq 'Error' -and $Exception) { $Message = ('{0} (Exception: {1})' -f $Message, $Exception) } $Line = ('[{0}] {1}: {2}' -f (Get-Date -Format o), $Level, $Message) if ($CackledaemonLogFile) { Add-Content $CackledaemonLogFile -Value $Line } if ($Level -eq 'Debug') { Write-Debug $Message } elseif ($Level -eq 'Verbose') { Write-Verbose $Message } elseif ($Level -eq 'Information') { Write-Information $Message } elseif ($Level -eq 'Host') { Write-Host $Message } elseif ($Level -eq 'Warning') { Write-Warning $Message } elseif ($Level -eq 'Error') { if ($Exception) { Write-Error -Message $Message ` -Exception $Exception ` -Category $Category ` -CategoryActivity $CategoryActivity ` -CategoryReason $CategoryReason ` -CategoryTargetName $CategoryTargetName ` -CategoryTargetType $CategoryTargetType } else { Write-Error -Message $Message ` -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-LogInformation { [Parameter(Position=0)] param([string]$Message) Write-Log $Message -Level Information } function Write-LogHost { [Parameter(Position=0)] param([string]$Message) Write-Log $Message -Level Host } 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 { param( [Parameter(Position=0)] [string]$Message, [Exception]$Exception, [System.Management.Automation.ErrorCategory]$Category = 'NotSpecified', [string]$CategoryActivity, [string]$CategoryReason, [string]$CategoryTargetName, [string]$CategoryTargetType ) if ($Exception) { Write-Log -Level Error ` -Message $Message ` -Exception $Exception ` -Category $Category ` -CategoryActivity $CategoryActivity ` -CategoryReason $CategoryReason ` -CategoryTargetName $CategoryTargetName ` -CategoryTargetType $CategoryTargetType } else { Write-Log -Level Error ` -Message $Message ` -Category $Category ` -CategoryActivity $CategoryActivity ` -CategoryReason $CategoryReason ` -CategoryTargetName $CategoryTargetName ` -CategoryTargetType $CategoryTargetType } } function Invoke-LogRotate { [CmdletBinding()] param() . $CackledaemonConfigLocation @($CackledaemonLogFile, $EmacsStdoutLogFile, $EmacsStdErrLogFile) | ForEach-Object { $LogFile = $_ if ((Test-Path $LogFile) -and (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-LogVerbose ('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 $LogFile, ($LogRotate + 1) if (Test-Path $StaleLogFile) { Write-LogVerbose ('Removing {0}...' -f $StaleLogFile) Remove-Item $StaleLogFile } Write-LogVerbose 'Done.' } } } function Enable-LogRotateJob { [CmdletBinding()] param() Enable-Job 'LogRotateJob' { . $CackledaemonConfigLocation while ($True) { Invoke-LogRotate Write-LogDebug ('LogRotateJob sleeping for {0} seconds.' -f $LogCheckTime) Start-Sleep -Seconds $LogCheckTime } } } function Disable-LogRotateJob { [CmdletBinding()] param() Disable-Job 'LogRotateJob' } 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( [Parameter(Position=0)] [int]$Major, [Parameter(Position=1)] [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) } Clear() { $this.Root = $null $this.Archives = $null $this.Installs = $null $this.Backups = $null } } 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 Remove-Workspace { $Workspace = Get-Workspace Remove-Item $Workspace.Root -Recurse $Workspace.Clear() } 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-Warning ('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-Verbose '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) { Set-Item -Path $RegistryPath -Value $BinPath Set-ItemProperty -Path $RegistryPath -Name Path -Value $Path } else { Write-Error -Message ("{0} doesn't exist - refusing to write this to the registry." -f $BinPath) ` -Category ObjectNotFound ` -CategoryActivity 'Set-EmacsAppPathRegistryKeys' ` -CategoryReason 'ItemNotFoundException'` -CategoryTargetName $BinPath ` -CategoryTargetType 'string' } } } 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-Shortcuts { . $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 Install-FileTypeAssociations { Get-FileTypeAssociationsConfig | ForEach-Object { cmd /c assoc ("{0}={1}" -f $_.Extension, $_.FileType) cmd /c ftype ("{0}={1}" -f $_.FileType, $_.Command) } } 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-LogHost "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-LogHost "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-LogHost "Installing shortcuts..." Install-Shortcuts Write-Progress -Id 3 -Activity 'Setting up the Emacs user environment' -Completed } function Install-Emacs { $ErrorActionPreference = 'Stop' Write-Progress -Id 1 -Activity 'Installing Emacs' -CurrentOperation 'Loading Cackledaemon configuration...' -PercentComplete 0 Write-LogVerbose 'Loading Cackledaemon configuration...' . $CackledaemonConfigLocation if (Test-Workspace) { $Workspace = Get-Workspace } else { Write-Progress -Id 1 -Activity 'Installing Emacs' -CurrentOperation 'Creating workspace...' -PercentComplete 14 Write-LogVerbose 'Creating new workspace...' $Workspace = New-Workspace } Write-Progress -Id 1 -Activity 'Installing Emacs' -CurrentOperation 'Checking the Emacs website for the latest available download...' -PercentComplete 29 Write-LogHost 'Checking the Emacs website for the latest available download...' $LatestDownload = Get-LatestEmacsDownload Write-LogVerbose ('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 43 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-LogVerbose ('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 57 if ($LatestDownload.Version -gt $InstalledVersion) { Write-LogVerbose ('Upstream Emacs version {0} is newer than installed Emacs version {1}' -f $LatestDownload.Version, $InstalledVersion) $ShouldInstall = $True } else { Write-LogVerbose ('Upstream Emacs version {0} is no newer than installed Emacs version {1}' -f $LatestDownload.Version, $InstalledVersion) } } else { Write-LogVerbose '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-LogHost 'Emacs is currently installed and at the latest available version.' } else { $TargetVersion = $LatestDownload.Version if ($Workspace.TestInstall($TargetVersion)) { Write-LogVerbose "Emacs has already been downloaded and unpacked for version $TargetVersion" $Install = $Workspace.GetInstall($TargetVersion) } else { if ($Workspace.TestArchive($TargetVersion)) { Write-LogVerbose "Eamcs 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 71 Write-LogHost "Downloading Emacs version $TargetVersion..." $Archive = New-EmacsArchive $LatestDownload } Write-Progress -Id 1 -Activity 'Installing Emacs' -CurrentOperation "Unpacking Emacs version $TargetVersion..." -PercentComplete 86 Write-LogHost "Unpacking Emacs version $TargetVersion..." $Install = Export-EmacsArchive $Archive } Write-Progress -Id 1 -Activity 'Installing Emacs' -CurrentOperation 'Executing Administrator commands...' -PercentComplete 60 Write-LogHost "Executing Administrator commands..." Start-Process -Wait powershell.exe -Verb RunAs -ArgumentList '-Command', "& { `$ErrorActionPreference = 'Stop' Import-Module Cackledaemon Write-Progress -Activity 'Installing Emacs' -CurrentOperation 'Moving files to $EmacsInstallLocation...' -PercentComplete 0 Write-LogHost 'Moving files to $EmacsInstallLocation...' Update-EmacsInstall -Path $Install Write-Progress -Activity 'Installing Emacs' -CurrentOperation 'Setting App Path registry keys...' -PercentComplete 1 Write-LogHost 'Setting App Path registry keys...' Set-EmacsAppPathRegistryKeys Write-Progress -Activity 'Installing Emacs' -CurrentOperation 'Setting file type associations...' -PercentComplete 2 Write-LogHost 'Setting file type associations...' Install-FileTypeAssociations Write-Progress -Activity 'Installing Emacs' -Completed }" Write-Progress -Id 1 -Activity 'Installing Emacs' -Completed Write-LogHost "Emacs $TargetVersion is installed and ready to rock!" } } function Test-ServerFileDirectory { . $CackledaemonConfigLocation Test-Path $ServerFileDirectory } function New-ServerFileDirectory { . $CackledaemonConfigLocation New-Item -Type Directory $ServerFileDirectory } function Clear-ServerFileDirectory { . $CackledaemonConfigLocation Get-ChildItem $ServerFileDirectory | ForEach-Object { Remove-Item $_.FullName } } 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) $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) { Write-LogError ` -Message 'The Emacs daemon is already running and being managed.' ` -Category ResourceExists ` -CategoryActivity 'Start-EmacsDaemon' ` -CategoryReason ManagedResourceExistsException } elseif (Get-UnmanagedEmacsDaemon) { 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-ProcessToPidFile $Process if ($Wait) { Write-Verbose 'Waiting for Emacs daemon to exit...' $Process = Wait-Process -Id $Process.Id } Write-Verbose 'Done.' return $Process } } function Get-EmacsDaemon { [CmdletBinding()] param() Get-EmacsProcessFromPidFile } function Stop-EmacsDaemon { [CmdletBinding()] param() $Process = Get-ProcessFromPidFile 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-ProcessToPidFile $null Write-LogVerbose 'Done.' } } function Restart-EmacsDaemon { [CmdletBinding()] param() try { Stop-EmacsDaemon -ErrorAction Stop } catch { Write-LogWarning 'Attempting to start the Emacs daemon even though stopping it failed' } Start-EmacsDaemon } 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 # 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 { Try { . $CackledaemonConfigLocation } Catch { Write-Warning 'Unable to load configuration! Using default notify timeout.' $NotifyTimeout = 5000 } 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 $RestartDaemonItem.Enabled = $True } else { $DaemonStatusItem.Text = '[STOPPED] Emacs Daemon' $StartDaemonItem.Enabled = $True $StopDaemonItem.Enabled = $False $RestartDaemonItem.Enabled = $True } $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 $EditConfigItem = New-Object System.Windows.Forms.MenuItem $EditConfigItem.Text = 'Edit Configuration...' $EditConfigItem.add_Click({ Start-InstrumentedBlock 'Failed to edit configuration' { Start-Process $CackledaemonConfigLocation } }) $ContextMenu.MenuItems.Add($EditConfigItem) | 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 start the Emacs daemon' { Start-EmacsDaemon -ErrorAction Stop } Start-InstrumentedBlock 'Failed to enable log rotation' { Enable-LogRotateJob -ErrorAction Stop } }) $ExitItem.add_Click({ if (Get-EmacsDaemon) { Start-InstrumentedBlock 'Failed to gracefully shut down Emacs' { Stop-EmacsDaemon -ErrorAction Stop } } if (Get-Job -Name 'LogRotateJob' -ErrorAction SilentlyContinue) { Start-InstrumentedBlock 'Failed to gracefully shut down log rotation' { 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 @( 'Clear-ServerFileDirectory', 'Disable-Job', 'Disable-LogRotateJob', 'Enable-Job', 'Enable-LogRotateJob', 'Export-EmacsArchive' 'Get-EmacsDaemon', 'Get-EmacsDownload', 'Get-EmacsExeVersion', 'Get-FileTypeAssociationsConfig', 'Get-LatestEmacsDownload', 'Get-ProcessFromPidFile', 'Get-ShortcutsConfig', 'Get-StartMenuItems', 'Get-StartMenuPath', 'Get-UnmanagedEmacsDaemon', 'Get-WShell', 'Get-Workspace', 'Install-Emacs', 'Install-EmacsUserEnvironment', 'Install-Shortcuts', 'Invoke-Applet', 'Invoke-LogRotate', 'New-CackledaemonWD', 'New-Download', 'New-EmacsArchive', 'New-ServerFileDirectory', 'New-Shortcut', 'New-Version', 'New-Workspace', 'Remove-Workspace', 'Restart-EmacsDaemon', 'Set-EmacsAppPathRegistryKeys', 'Set-EmacsPathEnvVariable', 'Set-HomeEnvVariable', 'Set-Shortcut', 'Start-EmacsDaemon', 'Stop-EmacsDaemon', 'Test-EmacsExe', 'Test-ServerFileDirectory', 'Update-EmacsInstall', 'Write-Log', 'Write-LogDebug', 'Write-LogError', 'Write-LogHost', 'Write-LogInformation', 'Write-LogVerbose', 'Write-LogWarning', 'Write-ProcessToPidFile' )` -Variable @( 'CackledaemonConfigLocation', 'CackledaemonWD' ) # Copyright 2020 Josh Holbrook # # This file is part of Cackledaemon and 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/>. |