很久沒寫 PowerShell,隨手找的練習題,幫自己設定一個打卡提醒,每天早上登入 Windows 時跳出提示,以免忘記打卡。(打卡程式設有自動提醒功能,但有時會失效)

原理很簡單,Windows 工作排程,除了指定執行時間,還可以設定「當任何使用者登入時執行」以及「當工作站由任何使用者解除鎖定時」觸發:

配上以下 PowerShell 程式,如此不管是冷機啟動登入,或是沒關電腦解除鎖定,只要是發生在打卡時間熱區,程式便會彈出提示:

# https://ss64.com/ps/messagebox.html
$periodSt = '07:30'
$periodEd = '09:30'
$now = Get-Date -Format 'HH:mm'
if ($now -le $periodSt -or $now -ge $periodEd) { Exit }

Add-Type -AssemblyName System.Windows.Forms,System.Drawing
$frm = New-Object System.Windows.Forms.Form
$frm.TopMost = $true
$frm.Text = '打卡提醒'
$frm.StartPosition = 'CenterScreen'
$frm.FormBorderStyle = 'FixedDialog'
$frm.MaximizeBox = $false
$frm.MinimizeBox = $false
$frm.ControlBox = $true
$frm.ShowInTaskbar = $true
$frm.BackColor = [System.Drawing.Color]::FromArgb(0xff, 0x2d, 0x2e, 0x30)
$img = [System.Drawing.Image]::FromFile("$PSScriptRoot\punch.png")
$picBox = New-Object System.Windows.Forms.PictureBox
$picBox.Image = $img
$picBox.SizeMode = 'AutoSize'
# 用 Add_Click() 設定點擊事件關閉視窗
$picBox.Add_Click({ $frm.Close() })
$frm.Controls.Add($picBox)
$frm.Width = $img.Width + 20
$frm.Height = $img.Height + 40
$frm.ShowDialog()

我原本打算用 Messagebox.Show(),缺點是可能被其他視窗覆蓋,最後我找到解法 - 在 PowerShell 建立 Windows Forms,設定 TopMost = $true 配合 ShowDialog(),確保提示視窗永遠浮在所有視窗上方。

既然都寫成 Window Form 了,索性用貼圖取代文字,讓提示更有趣一些,並設計成點圖片可以關視窗。(學到用 Add_Click() 設定事件的寫法)

如下圖所示,提示視窗甚至可以跑在工作管理員的上方,想不被注意都難。

原本程式寫完手動設定排程到這裡該結束了。但我再加碼練習怎麼用 PowerShell 取代手工設定排程,此時遇上小問題 - PowerShell Cmdlet 不支援「當工作站由任何使用者解除鎖定時」觸發時機,猜猜 ChatGPT 能不能正確回答?

依我上回發明的 ChatGPT 命中率預測法 - 這個問題是否去街上隨便找個會寫 PowerShell 的老鳥問都會解?依其冷門程度,答案為否,故預測 ChatGPT 無法正確解答。

詢問結果,New-ScheduledTaskTrigger -AtLogon 是對的,但 ChatGPT 幻想出不存在的 -AtUnlock:

經指正後,再回答了另一個不存在 -SessionStateChange RemoteConnect (但跟正確答案 CIM 類別 MSFT_TaskSessionStateChangeTrigger 已沾上邊)

實測得證,此一問題不夠熱門,直接 Google 較快。

最後寫出設定排程腳本如下,解除鎖定觸發器需用 Get-CimClass 建立: (程式還有用到自動偵測改以管理者身分執行的技巧)

Param ([string]$ForceOverwrite = 'N')
$taskName = "打卡提醒"
$ErrorActionPreference = "STOP"

# 
$wp = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
if (-Not $wp.IsInRole([Security.Principal.WindowsBuiltInRole]"Administrator")) {
    $rawCmd = $MyInvocation.Line
    $rawArgs = $rawCmd.Substring($rawCmd.IndexOf('.ps1') + 4)
	# When run with file explorer context menu,
	# if((Get-ExecutionPolicy ) -ne 'AllSigned') { Set-ExecutionPolicy -Scope Process Bypass }; & 'D:\Restart-WinService.ps1'
	if ($rawCmd.StartsWith('if')) { $rawArgs = '' }
    Start-Process Powershell -Verb RunAs -ArgumentList "$PSCommandPath $rawArgs" 
}
else {

    $chkExist = Get-ScheduledTask | Where-Object { $_.TaskName -eq $taskName }
    if ($chkExist) {
        if ($ForceOverwrite -eq 'Y' -or $(Read-Host "[$taskName] 已存在,是否刪除? (Y/N)").ToUpper() -eq 'Y') {
            Unregister-ScheduledTask $taskName -Confirm:$false 
        }
        else {
            Write-Host "放棄新增作業" -ForegroundColor Red
            Exit 
        }
    }
    # 登入時觸發
    $logonTrigger = New-ScheduledTaskTrigger -AtLogOn

    # 解除鎖定時觸發,PowerShell 不支援,必須使用 CIM 類別
    # REF: https://stackoverflow.com/a/53704779/288936
    $cimClass = Get-CimClass `
        -Namespace ROOT\Microsoft\Windows\TaskScheduler `
        -ClassName MSFT_TaskSessionStateChangeTrigger
    $unlockTrigger = New-CimInstance -CimClass $cimClass -ClientOnly -Property @{
        StateChange  = 8 # TASK_SESSION_STATE_CHANGE_TYPE.TASK_SESSION_UNLOCK
    }

    # 執行動作
    $action = New-ScheduledTaskAction -Execute "powershell.exe" `
        -Argument "-NoProfile -WindowStyle Hidden -Command `"$PSScriptRoot\punchreminder.ps1`""

    # 設定用當時登入的帳號執行
    $principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive

    Register-ScheduledTask -TaskName $taskName -Action $action -Trigger ($logonTrigger,$unlockTrigger) -Principal $principal
}

伸展完畢。

Example of how to use PowerShell create a Windows Form and show it in front of any desktop applications and how to set it as scheduled task triggered on user logon or unlock the Windows.


Comments

Be the first to post a comment

Post a comment