diff --git a/cabbie.go b/cabbie.go index 5ee9afe..9a35e68 100644 --- a/cabbie.go +++ b/cabbie.go @@ -310,6 +310,7 @@ func setRebootMetric() { } func enforce() error { + ctx := context.Background() updates, err := enforcement.Get() if err != nil { return fmt.Errorf("error retrieving required updates: %v", err) @@ -320,7 +321,7 @@ func enforce() error { var failures error if len(updates.Required) > 0 { i := installCmd{kbs: strings.Join(updates.Required, ",")} - if err := i.installUpdates(); err != nil { + if err := i.installUpdates(ctx); err != nil { failures = fmt.Errorf("error enforcing required updates: %v", err) deck.ErrorA(failures).With(eventID(cablib.EvtErrInstallFailure)).Go() } @@ -350,6 +351,7 @@ func initDriverExclusion() error { } func runMainLoop() error { + ctx := context.Background() if err := notification.CleanNotifications(cablib.SvcName); err != nil { deck.ErrorfA("Error clearing old notifications:\n%v", err).With(eventID(cablib.EvtErrNotifications)).Go() } @@ -400,7 +402,7 @@ func runMainLoop() error { select { case <-t.Default.C: i := installCmd{Interactive: false} - err := i.installUpdates() + err := i.installUpdates(ctx) if err != nil { deck.ErrorfA("Error installing system updates:\n%v", err).With(eventID(cablib.EvtErrInstallFailure)).Go() } @@ -445,7 +447,7 @@ func runMainLoop() error { if trimmedOpen.Before(now) && trimmedClose.After(now) && ((today >= maintOpenDay) && (today <= maintCloseDay)) { deck.InfofA("Active Hours + Maintenance window open: Starting installation process.").With(eventID(cablib.EvtInstall)).Go() i := installCmd{Interactive: false} - err := i.installUpdates() + err := i.installUpdates(ctx) if err != nil { deck.ErrorfA("Error installing system updates:\n%v", err).With(eventID(cablib.EvtErrInstallFailure)).Go() } @@ -461,7 +463,7 @@ func runMainLoop() error { if s[0].State == "open" { deck.InfofA("Maintenance window open: Starting installation process.").With(eventID(cablib.EvtInstall)).Go() i := installCmd{Interactive: false} - err := i.installUpdates() + err := i.installUpdates(ctx) if err != nil { deck.ErrorfA("Error installing system updates:\n%v", err).With(eventID(cablib.EvtErrInstallFailure)).Go() } @@ -503,15 +505,15 @@ func runMainLoop() error { if config.Deadline != 0 { i := installCmd{Interactive: false, deadlineOnly: true} - if err := i.installUpdates(); err != nil { + if err := i.installUpdates(ctx); err != nil { deck.ErrorfA("Error installing system updates:\n%v", err).With(eventID(cablib.EvtErrInstallFailure)).Go() } } case <-t.Virus.C: i := installCmd{Interactive: false, virusDef: true} - err := i.installUpdates() + err := i.installUpdates(ctx) if e := virusUpdateSuccess.Set(err == nil); e != nil { - deck.ErrorfA("Error posting virusUpdateSuccess metric:\n%v", err).With(eventID(cablib.EvtErrMetricReport)).Go() + deck.ErrorfA("Error posting virusUpdateSuccess metric:\n%v", e).With(eventID(cablib.EvtErrMetricReport)).Go() } if err != nil { deck.ErrorfA("Error installing virus definitions:\n%v", err).With(eventID(cablib.EvtErrInstallFailure)).Go() @@ -519,7 +521,7 @@ func runMainLoop() error { } case <-t.Driver.C: i := installCmd{Interactive: false, drivers: true} - err := i.installUpdates() + err := i.installUpdates(ctx) if e := driverUpdateSuccess.Set(err == nil); e != nil { deck.ErrorfA("Error posting driverUpdateSuccess metric:\n%v", e).With(eventID(cablib.EvtErrMetricReport)).Go() } diff --git a/install.go b/install.go index bb5a1a7..1aabda8 100644 --- a/install.go +++ b/install.go @@ -14,10 +14,12 @@ package main import ( + "bytes" "golang.org/x/net/context" "errors" "fmt" "os" + "os/exec" "path/filepath" "strings" "time" @@ -86,12 +88,12 @@ func vetFlags(i installCmd) error { return nil } -func (i installCmd) Execute(_ context.Context, flags *flag.FlagSet, _ ...any) subcommands.ExitStatus { +func (i installCmd) Execute(ctx context.Context, flags *flag.FlagSet, _ ...any) subcommands.ExitStatus { if err := vetFlags(i); err != nil { return subcommands.ExitUsageError } - if err := i.installUpdates(); err != nil { + if err := i.installUpdates(ctx); err != nil { fmt.Printf("Failed to install updates: %v", err) deck.ErrorfA("Failed to install updates: %v", err).With(eventID(cablib.EvtErrInstallFailure)).Go() return subcommands.ExitFailure @@ -165,6 +167,34 @@ func downloadCollection(s *session.UpdateSession, c *updatecollection.Collection return d.ResultCode() } +// fetchDetailedUpdateError queries the Windows Event Log for recent update installation +// failures matching the given title and returns any specific error code found in +// the event message and true, or empty string and false if not found or on error. +func fetchDetailedUpdateError(ctx context.Context, title string) (string, bool) { + // Query for event ID 20 from Microsoft-Windows-WindowsUpdateClient in the System log + // within the last 5 minutes. Filter messages that contain the update title. + // Sort by newest first, take the first result, and extract the first + // hexadecimal code found in the message, if any. + psTitle := "'" + strings.ReplaceAll(title, "'", "''") + "'" + psCmd := fmt.Sprintf(`Get-WinEvent -FilterHashtable @{LogName='System';ProviderName='Microsoft-Windows-WindowsUpdateClient';Id=20;StartTime=(Get-Date).AddMinutes(-5)} -ErrorAction SilentlyContinue |Where-Object {$_.Message -match [regex]::Escape(%s)} |Sort-Object TimeCreated -Descending |Select-Object -First 1 |ForEach-Object { if($_.Message -match '(0x[0-9a-fA-F]+)'){ $matches[1] } }`, psTitle) + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`, "-NoProfile", "-Command", psCmd) + var out bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + deck.WarningfA("Failed to execute PowerShell to get detailed update error for %q: %v, stderr: %q", title, err, stderr.String()).Go() + return "", false + } + code := strings.TrimSpace(out.String()) + if code == "" { + return "", false + } + return code, true +} + func installCollection(s *session.UpdateSession, c *updatecollection.Collection, ipu bool) (*installRsp, error) { inst, err := install.NewInstaller(s, c) if err != nil { @@ -204,7 +234,7 @@ func installCollection(s *session.UpdateSession, c *updatecollection.Collection, }, err } -func (i *installCmd) installUpdates() error { +func (i *installCmd) installUpdates(ctx context.Context) error { // If monthly patches are disabled, and no specific update type was requested, do nothing. if config.InstallMonthlyPatches == 0 && !i.all && !i.drivers && !i.virusDef && i.kbs == "" { deck.InfoA("InstallMonthlyPatches is disabled, skipping default update installation.").With(eventID(cablib.EvtMisc)).Go() @@ -410,6 +440,9 @@ outerLoop: deck.InfofA("Successfully installed update:\n%s\nHResult Code: %s", u.Title, rsp.hResult).With(eventID(cablib.EvtInstall)).Go() } else { deck.ErrorfA("Failed to install update:\n%s\nReturnCode: %d\nHResult Code: %s", u.Title, rsp.resultCode, rsp.hResult).With(eventID(cablib.EvtErrInstallFailure)).Go() + if code, ok := fetchDetailedUpdateError(ctx, u.Title); ok { + deck.WarningfA("Detailed error for update %q from Windows Update Client log: %s", u.Title, code).Go() + } c.Close() continue }