cli_tools/diagnostics/main_windows.go (315 lines of code) (raw):

// Copyright 2018 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "errors" "fmt" "log" "os" "os/exec" "path/filepath" "strings" "time" ) const ( eventLogsRoot = `C:\Windows\System32\winevt\Logs` k8sLogsRoot = `C:\etc\kubernetes\logs` // TODO: user can change the dump path, so better fetch the path from Registry: // https://support.microsoft.com/en-us/help/254649/overview-of-memory-dump-file-options-for-windows // But it's not likely people will do that. crashDump = `C:\Windows\MEMORY.dmp` rdpStatusFileName = "rdp_status.txt" rdpScriptFileName = "rdp_status.ps1" dockerImageListFileName = "docker_images.log" ) // struct used to construct `Get-WinEvent (-ProviderName/-LogName) ${logName}` cmd string. type winEvt struct { logName string // If event logs are from provider i.e. docker, should be True, // Otherwise should be false, i.e. Application, System, etc. fromProvider bool } type cmd struct { path string args string outputFileName string // True when the command produces its own file and doesn't need one // created from stdout. cmdProducesFile bool } type wmiQuery struct { class string namespace string outputFileName string } func (command cmd) run() (outPath string, err error) { outPath = filepath.Join(tmpFolder, command.outputFileName) c := exec.Command(command.path) argString := command.args if command.cmdProducesFile { // Replace any output file args with that path in a temp folder relPath := command.outputFileName argString = strings.Replace(argString, relPath, outPath, -1) } else { // If the command doesn't produce a file, we need to construct // one from Stdout and Stderr outFile, err := os.Create(outPath) if err != nil { log.Printf("Error creating file %s: %v", outPath, err) return outPath, err } defer func() { if cErr := outFile.Close(); err != nil { err = cErr } }() c.Stdout = outFile c.Stderr = outFile } if command.args != "" { args := strings.Split(argString, " ") for _, arg := range args { // Decode the "%20" to space c.Args = append(c.Args, strings.ReplaceAll(arg, "%20", " ")) } } err = c.Run() return } func (query wmiQuery) run() (string, error) { outPath := filepath.Join(tmpFolder, query.outputFileName) outFile, err := os.Create(outPath) if err != nil { return outPath, err } defer outFile.Close() // WMI is somewhat flaky, so we should retry a few times on failures var data string for i := 0; i < 3; i++ { data, err = printWmiObjects(query.class, query.namespace) if err == nil { break } } if err != nil { return outPath, err } header := fmt.Sprintf("Queried wmi objects [%s] from namespace %s\n\n", query.class, query.namespace) if _, err = outFile.WriteString(header); err != nil { return outPath, err } _, err = outFile.WriteString(data) return outPath, err } func runAll(commands []runner, errCh chan error) []string { paths := make([]string, 0, len(commands)) for _, command := range commands { path, err := command.run() if err != nil { errCh <- fmt.Errorf("Error: %s while running %v", err, command) } else { paths = append(paths, path) } } return paths } func gatherSystemLogs(logs chan logFolder, errs chan error) { var commands = []runner{ cmd{`C:\Windows\System32\systeminfo.exe`, "", "systeminfo.txt", false}, cmd{`C:\Windows\System32\bcdedit.exe`, "", "bcdedit.txt", false}, cmd{`C:\Windows\System32\sc.exe`, "query type=driver", "drivers.txt", false}, cmd{`C:\Windows\System32\pnputil.exe`, "/e", "pnputil.txt", false}, cmd{`C:\Windows\System32\msinfo32.exe`, "/report msinfo32.txt", "msinfo32.txt", true}, wmiQuery{"Win32_UserAccount", `root\CIMv2`, "users.txt"}, } logs <- logFolder{"System", runAll(commands, errs)} } func gatherDiskLogs(logs chan logFolder, errs chan error) { var commands = []runner{ wmiQuery{"MSFT_Disk", `root\Microsoft\Windows\Storage`, "disks.txt"}, wmiQuery{"MSFT_Volume", `root\Microsoft\Windows\Storage`, "volumes.txt"}, wmiQuery{"MSFT_Partition", `root\Microsoft\Windows\Storage`, "partitions.txt"}, } logs <- logFolder{"Disk", runAll(commands, errs)} } func gatherNetworkLogs(logs chan logFolder, errs chan error) { var commands = []runner{ cmd{`C:\Windows\System32\nslookup.exe`, "8.8.8.8", "nslookup_dns.txt", false}, cmd{`C:\Windows\System32\tracert.exe`, "www.gstatic.com", "tracert_gstatic.txt", false}, cmd{`C:\Windows\System32\ping.exe`, "-n 10 8.8.8.8", "ping_dns.txt", false}, cmd{`C:\Windows\System32\ping.exe`, "-n 10 www.gstatic.com", "ping_gstatic.txt", false}, cmd{`C:\Windows\System32\ipconfig.exe`, "/all", "ipconfig.txt", false}, cmd{`C:\Windows\System32\route.exe`, "print", "route.txt", false}, cmd{`C:\Windows\System32\netstat.exe`, "-anb", "netstat.txt", false}, wmiQuery{"MSFT_NetFirewallRule", `root\StandardCimv2`, "firewall.txt"}, } logs <- logFolder{"Network", runAll(commands, errs)} } func gatherProgramLogs(logs chan logFolder, errs chan error) { var commands = []runner{ wmiQuery{"Win32_Process", `root\Cimv2`, "processes.txt"}, wmiQuery{"Win32_Service", `root\Cimv2`, "services.txt"}, wmiQuery{"MSFT_ScheduledTask", `root\Microsoft\Windows\TaskScheduler`, "scheduled_tasks.txt"}, } logs <- logFolder{"Program", runAll(commands, errs)} } // gatherRDPSettings invokes rdp_status.ps1 to collect rdp settings, // return rdp_status.txt file path and errors(if any). func gatherRDPSettings(logs chan logFolder, errs chan error) { pwshPath, err := exec.LookPath("powershell") if err != nil { errs <- err return } // Get current runtime directory path absDir, err := filepath.Abs(filepath.Dir(os.Args[0])) if err != nil { errs <- err return } rdpScriptFilePath := filepath.Join(absDir, rdpScriptFileName) // As we splits the params by spaces, but the rdp script file path has spaces, // So encode the space to "%20" to keep the file path as a whole, then decode // it back to space later on. rdpScriptFilePath = strings.ReplaceAll(rdpScriptFilePath, " ", "%20") var commands = []runner{ cmd{pwshPath, "-ExecutionPolicy Unrestricted -File " + rdpScriptFilePath, rdpStatusFileName, false}, } logs <- logFolder{"RDP", runAll(commands, errs)} } // collectFilePaths recursively collect all the file paths under given list of roots, // return list of file paths and errors(if any). func collectFilePaths(roots []string) ([]string, []error) { filePaths := make([]string, 0) errs := make([]error, 0) for _, root := range roots { // Compared filepath.Walk with orginal BFS folder traversal using Measure-Command cmdlet, // looks like almost the same. // filepath.Walk -> 4s 973ms // original BFS folder traversal -> 4s 897ms // Although filepath.Walk is slower than `find` due to extra lstat calls // https://github.com/golang/go/issues/16399, it should be good enough for this scenario. err := filepath.Walk(root, func(path string, info os.FileInfo, e error) error { if e != nil { return e } if !info.IsDir() { filePaths = append(filePaths, path) } return nil }) if err != nil { errs = append(errs, err) } } return filePaths, errs } // Returns a Get-WinEvent command to get the event log for the specified log source. func getCommandToConvertEvt(evt winEvt) string { cmdStr := `Get-WinEvent %s'%s' | Format-Table -Auto -Wrap` source := "-LogName " if evt.fromProvider { source = "-ProviderName " } return fmt.Sprintf(cmdStr, source, evt.logName) } // getPlainEventLogs generates plain text event logs thru `Get-WinEvent` powershell cmd, // return list file paths and errors in error channel. func getPlainEventLogs(evts []winEvt, errs chan error) []string { pwshPath, err := exec.LookPath("powershell") if err != nil { errs <- err return []string{} } commands := make([]runner, 0) for _, evt := range evts { commands = append(commands, cmd{ path: pwshPath, args: getCommandToConvertEvt(evt), outputFileName: evt.logName + ".log", cmdProducesFile: false}) } return runAll(commands, errs) } // getDockerImagesList put docker images list file in logFolder channel and errors in error channel. func getDockerImagesList(logs chan logFolder, errs chan error) { dockerImageFilePath := make([]string, 0, 1) dockerPath, err := exec.LookPath("docker") if err != nil { log.Printf("Docker not installed, couldn't get docker images list.\n") } else { var commands = []runner{ cmd{dockerPath, "image list", dockerImageListFileName, false}, } dockerImageFilePath = runAll(commands, errs) } logs <- logFolder{"Kubernetes", dockerImageFilePath} } // gatherEventLogs put all the event log file paths in logFolder channel // and errors in error channel. func gatherEventLogs(logs chan logFolder, errs chan error) { roots := []string{eventLogsRoot} filePaths, ers := collectFilePaths(roots) for _, err := range ers { errs <- err } plainEvtLogPaths := getPlainEventLogs([]winEvt{ {logName: "Application", fromProvider: false}, {logName: "System", fromProvider: false}, }, errs) filePaths = append(filePaths, plainEvtLogPaths...) logs <- logFolder{"Event", filePaths} } // gatherKubernetesLogs put all the kubernetes log file paths in logFolder channel // and errors in error channel. func gatherKubernetesLogs(logs chan logFolder, errs chan error) { roots := make([]string, 0) var err error _, err = os.Stat(k8sLogsRoot) if err == nil { roots = append(roots, k8sLogsRoot) } else if os.IsNotExist(err) { log.Printf("%s doesn't exists, no need to collect inside logs.\n", k8sLogsRoot) } else { log.Printf("unexpected error happened when check existence of %s: %v", k8sLogsRoot, err) } _, err = os.Stat(crashDump) if err == nil { roots = append(roots, crashDump) } else if os.IsNotExist(err) { log.Printf("%s doesn't exists, no need to collect.\n", crashDump) } else { log.Printf("unexpected error happened when check existence of %s: %v", crashDump, err) } // Collect the kubernetes logs and crash dump filePaths := make([]string, 0) if len(roots) > 0 { var ers []error filePaths, ers = collectFilePaths(roots) for _, err := range ers { errs <- err } } // Collect docker event logs _, err = exec.LookPath("docker") if err == nil { plainEvtLogPaths := getPlainEventLogs([]winEvt{ {logName: "docker", fromProvider: true}, }, errs) filePaths = append(filePaths, plainEvtLogPaths...) } else { log.Printf("Docker not installed, no docker logs.\n") } logs <- logFolder{"Kubernetes", filePaths} } func gatherTraceLogs(logs chan logFolder, errs chan error) { traceStart := cmd{`C:\Windows\System32\wpr.exe`, "-start CPU -start DiskIO -start FileIO -start Network", "trace.etl", true} traceStop := cmd{`C:\Windows\System32\wpr.exe`, "-stop trace.etl", "trace.etl", true} if _, err := traceStart.run(); err != nil { errs <- err } time.Sleep(10 * time.Minute) paths := runAll([]runner{ traceStop, }, errs) logs <- logFolder{"Trace", paths} } func gatherLogs(trace bool) ([]logFolder, error) { runFuncs := []func(logs chan logFolder, errs chan error){ gatherSystemLogs, gatherDiskLogs, gatherNetworkLogs, gatherProgramLogs, gatherEventLogs, gatherKubernetesLogs, gatherRDPSettings, getDockerImagesList, } if trace { runFuncs = append(runFuncs, gatherTraceLogs) } folderCount := len(runFuncs) folders := make([]logFolder, 0, folderCount) errStrings := make([]string, 0) ch := make(chan logFolder, folderCount) errs := make(chan error) for _, run := range runFuncs { go run(ch, errs) } for { select { case folder := <-ch: folders = append(folders, folder) case err := <-errs: errStrings = append(errStrings, err.Error()) } if len(folders) == folderCount { break } } if len(errStrings) > 0 { return folders, errors.New(strings.Join(errStrings, "\n")) } return folders, nil }