Cackledaemon.psm1

class CackledaemonException: System.Exception {
    CackledaemonException([string]$Message) : base($Message) {}
}

class CackledaemonAlreadyRunningException: CackledaemonException {
    CackledaemonAlreadyRunningException([string]$Message) : base($Message) {}
}

class CackledaemonNotRunningException: CackledaemonException {
    CackledaemonNotRunningException([string]$Message) : base($Message) {}
}

$CackledaemonWD = Join-Path $env:APPDATA 'cackledaemon'

function Ensure-CackledaemonWD {
    If (-not (Test-Path $CackledaemonWD)) {
        New-Item -Path $CackledaemonWD -ItemType directory
    }
}

$CackledaemonLogFile = Join-Path $CackledaemonWD 'log.log'
$CackledaemonLogSize = 16
$CackledaemonLogRotate = 4
$CackledaemonLogCheckTime = 2  # Seconds

function Write-CackledaemonLog {
    Param ([string]$Message)

    Ensure-CackledaemonWD

    $Line = ('[{0}] CACKLEDAEMON: {1}' -f (Get-Date -Format o), $Message)

    Add-Content $CackledaemonLogFile -value $Line
}

function Start-CackledaemonLogRotateJob {
    Start-Job `
    -Name 'CackledaemonLogRotateJob' `
    -InitializationScript {
        Import-Module Cackledaemon
    }
    -ScriptBlock {
        Set-Location $CackledaemonWD

        Write-CackledaemonLog "{0} {1}" -f (Get-Item $CackledaemonLogFile).Length, $CackledaemonLogSize

        while ($true) {
            If ((Get-Item $CackledaemonLogFile).Length -ge $CackledaemonLogSize) {
                Write-CackledaemonLog 'Rotating logs...'

                ($CackledaemonLogRotate..0) | ForEach-Object {
                    $Current = Join-Path `
                        $CackledaemonWD `
                        $(If ($_) { 'log.log.{0}' -f $_ } Else { 'log.log' })

                    $Next = Join-Path $CackledaemonWD ('log.log{0}' -f ($_ + 1))

                    If (Test-Path $Current) {
                        Write-CackledaemonLog ('Copying {0} to {1}...' -f $Current, $Next)

                        Copy-Item -Path $Current -Destination $Next
                    }
                }

                Write-CackledaemonLog ('Truncating {0}...' -f $CackledaemonLogFile)

                Clear-Content $CackledaemonLogFile

                $StaleLogFile = Join-Path `
                  $CackledaemonWD `
                  ('log.log.{0}' -f ($CackledaemonLogRotate + 1))

                If (Test-Path $StaleLogFile) {
                    Write-CackledaemonLog ('Removing {0}...' -f $StaleLogFile)

                    Remove-Item $StaleLogFile
                }

                Write-CackledaemonLog 'Done.'
            }
            Write-CackledaemonLog 'All quiet on the Western front...'
            Start-Sleep -Seconds $CackledaemonLogCheckTime
        }
    }
}

$CackledaemonProcessStateFile = Join-Path $CackledaemonWD "DaemonProcessState.json"

function Write-ProcessState {
    param([System.Diagnostics.Process]$Process)

    $Process | ConvertTo-Json | Out-File $CackledaemonProcessStateFile
}

function Get-ProcessState {
    $Id = (Get-Content $CackledaemonProcessStateFile | ConvertFrom-Json).Id

    If (-not $Id) {
        return $null
    }

    return Get-Process -Id $Id
}

function Get-UnmanagedEmacsDaemons () {
    $ManagedProcess = $(Retrieve-ProcessState)
    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 {
    $Process = $(Get-ProcessState)

    If ($Process) {
        Throw [CackledaemonAlreadyRunningException]::new(
            "The Emacs daemon is already running and being managed!"
        )
    }

    If ($(Get-UnmanagedEmacsDaemons)) {
        Throw [CackledaemonAlreadyRunningException]::new(
            "The Emacs daemon has already been started by someone else and " +
            "is not being managed!"
        )
    }

    Write-CackledaemonLog "Starting the Emacs daemon..."

    $Process = Start-Process `
      -FilePath "emacs.exe" `
      -ArgumentList "--daemon" `
      -NoNewWindow `
      -RedirectStandardOut $logFile `
      -RedirectStandardError $logFile `
      -PassThru

    Write-CackledaemonLog "Saving the Emacs daemon's process state..."

    Write-ProcessState -Process $Process

    Write-CackledaemonLog "Done."

    return $Process
}

function Stop-EmacsDaemon {
    $Process = Retrieve-ProcessState

    If (-not $Process) {
        Throw [CackledaemonNotRunningException]::new(
            "A managed Emacs daemon isn't running and can not be stopped!"
        )
    }

    Write-CackledaemonLog "Stopping the Emacs daemon..."

    Stop-Process -InputObject $Process

    Store-ProcessState $null

    Write-CackledaemonLog "Done."
}

function Restart-EmacsDaemon {
    Stop-EmacsDaemon
    Start-EmacsDaemon
}

Export-ModuleMember `
  -Function @(
      'Ensure-CackledaemonWD',
      'Write-CackledaemonLog',
      'Start-CackledaemonLogRotateJob',
      'Start-EmacsDaemon',
      'Stop-EmacsDaemon',
      'Restart-EmacsDaemon',
      'Write-ProcessState',
      'Get-ProcessState',
      'Get-UnmanagedEmacsDaemons'
  ) `
  -Variable @(
      'CackledaemonWD',
      'CackledaemonLogFile',
      'CackledaemonLogSize',
      'CackledaemonLogRotate',
      'CackledaemonLogCheckTime',
      'CackledaemonProcessStateFile'
  )