| // The pstore package provides functions for interfacing with the Linux kernel's | 
 | // pstore (persistent storage) system. | 
 | // Documentation for pstore itself can be found at | 
 | // https://docs.kernel.org/admin-guide/abi-testing.html#abi-sys-fs-pstore. | 
 | package pstore | 
 |  | 
 | import ( | 
 | 	"bufio" | 
 | 	"errors" | 
 | 	"fmt" | 
 | 	"io/fs" | 
 | 	"os" | 
 | 	"path/filepath" | 
 | 	"regexp" | 
 | 	"sort" | 
 | 	"strconv" | 
 | 	"time" | 
 | ) | 
 |  | 
 | // CanonicalMountPath contains the canonical mount path of the pstore filesystem | 
 | const CanonicalMountPath = "/sys/fs/pstore" | 
 |  | 
 | // pstoreDmesgHeader contains parsed header data from a pstore header. | 
 | type pstoreDmesgHeader struct { | 
 | 	Reason  string | 
 | 	Counter uint64 | 
 | 	Part    uint64 | 
 | } | 
 |  | 
 | var headerRegexp = regexp.MustCompile("^([^#]+)#([0-9]+) Part([0-9]+)$") | 
 |  | 
 | // parseDmesgHeader parses textual pstore entry headers as assembled by | 
 | // @linux//fs/pstore/platform.c:pstore_dump back into a structured format. | 
 | // The input must be the first line of a file with the terminating \n stripped. | 
 | func parseDmesgHeader(hdr string) (*pstoreDmesgHeader, error) { | 
 | 	parts := headerRegexp.FindStringSubmatch(hdr) | 
 | 	if parts == nil { | 
 | 		return nil, errors.New("unable to parse pstore entry header") | 
 | 	} | 
 | 	counter, err := strconv.ParseUint(parts[2], 10, 64) | 
 | 	if err != nil { | 
 | 		return nil, fmt.Errorf("failed to parse pstore header count: %w", err) | 
 | 	} | 
 | 	part, err := strconv.ParseUint(parts[3], 10, 64) | 
 | 	if err != nil { | 
 | 		return nil, fmt.Errorf("failed to parse pstore header part: %w", err) | 
 | 	} | 
 | 	return &pstoreDmesgHeader{ | 
 | 		Reason:  parts[1], | 
 | 		Counter: counter, | 
 | 		Part:    part, | 
 | 	}, nil | 
 | } | 
 |  | 
 | // A reassembled kernel message buffer dump from pstore. | 
 | type KmsgDump struct { | 
 | 	// The reason why the dump was created. Common values include "Panic" and | 
 | 	// "Oops", but depending on the setting `printk.always_kmsg_dump` and | 
 | 	// potential future reasons this is likely unbounded. | 
 | 	Reason string | 
 | 	// The CLOCK_REALTIME value of the first entry in the dump (which is the | 
 | 	// closest to the actual time the dump happened). This can be zero or | 
 | 	// garbage if the RTC hasn't been initialized or the system has no working | 
 | 	// clock source. | 
 | 	OccurredAt time.Time | 
 | 	// A counter counting up for every dump created. Can be used to order dumps | 
 | 	// when the OccurredAt value is not usable due to system issues. | 
 | 	Counter uint64 | 
 | 	// A list of kernel log lines in oldest-to-newest order, i.e. the oldest | 
 | 	// message comes first. The actual cause is generally reported last. | 
 | 	Lines []string | 
 | } | 
 |  | 
 | var dmesgFileRegexp = regexp.MustCompile("^dmesg-.*-([0-9]+)") | 
 |  | 
 | type pstoreDmesgFile struct { | 
 | 	hdr   pstoreDmesgHeader | 
 | 	ctime time.Time | 
 | 	lines []string | 
 | } | 
 |  | 
 | // GetKmsgDumps returns a list of events where the kernel has dumped its kmsg | 
 | // (kernel log) buffer into pstore because of a kernel oops or panic. | 
 | func GetKmsgDumps() ([]KmsgDump, error) { | 
 | 	return getKmsgDumpsFromFS(os.DirFS(CanonicalMountPath)) | 
 | } | 
 |  | 
 | // f is injected here for testing | 
 | func getKmsgDumpsFromFS(f fs.FS) ([]KmsgDump, error) { | 
 | 	var events []KmsgDump | 
 | 	eventMap := make(map[string][]pstoreDmesgFile) | 
 | 	pstoreEntries, err := fs.ReadDir(f, ".") | 
 | 	if err != nil { | 
 | 		return events, fmt.Errorf("failed to list files in pstore: %w", err) | 
 | 	} | 
 | 	for _, entry := range pstoreEntries { | 
 | 		if !dmesgFileRegexp.MatchString(entry.Name()) { | 
 | 			continue | 
 | 		} | 
 | 		f, err := f.Open(entry.Name()) | 
 | 		if err != nil { | 
 | 			return events, fmt.Errorf("failed to open pstore entry file: %w", err) | 
 | 		} | 
 | 		// This only closes after all files have been read, but the number of | 
 | 		// files is heavily bound by very small amounts of pstore space. | 
 | 		defer f.Close() | 
 | 		finfo, err := f.Stat() | 
 | 		if err != nil { | 
 | 			return events, fmt.Errorf("failed to stat pstore entry file: %w", err) | 
 | 		} | 
 | 		s := bufio.NewScanner(f) | 
 | 		if !s.Scan() { | 
 | 			return events, fmt.Errorf("cannot read first line header of pstore entry %q: %w", entry.Name(), s.Err()) | 
 | 		} | 
 | 		hdr, err := parseDmesgHeader(s.Text()) | 
 | 		if err != nil { | 
 | 			return events, fmt.Errorf("failed to parse header of file %q: %w", entry.Name(), err) | 
 | 		} | 
 | 		var lines []string | 
 | 		for s.Scan() { | 
 | 			lines = append(lines, s.Text()) | 
 | 		} | 
 | 		// Same textual encoding is used in the header itself, so this | 
 | 		// is as unique as it gets. | 
 | 		key := fmt.Sprintf("%v#%d", hdr.Reason, hdr.Counter) | 
 | 		eventMap[key] = append(eventMap[key], pstoreDmesgFile{hdr: *hdr, ctime: finfo.ModTime(), lines: lines}) | 
 | 	} | 
 |  | 
 | 	for _, event := range eventMap { | 
 | 		sort.Slice(event, func(i, j int) bool { | 
 | 			return event[i].hdr.Part > event[j].hdr.Part | 
 | 		}) | 
 | 		ev := KmsgDump{ | 
 | 			Counter: event[len(event)-1].hdr.Counter, | 
 | 			Reason:  event[len(event)-1].hdr.Reason, | 
 | 			// Entries get created in reverse order, so the most accurate | 
 | 			// timestamp is the first one. | 
 | 			OccurredAt: event[len(event)-1].ctime, | 
 | 		} | 
 | 		for _, entry := range event { | 
 | 			ev.Lines = append(ev.Lines, entry.lines...) | 
 | 		} | 
 | 		events = append(events, ev) | 
 | 	} | 
 | 	sort.Slice(events, func(i, j int) bool { | 
 | 		return !events[i].OccurredAt.Before(events[j].OccurredAt) | 
 | 	}) | 
 | 	return events, nil | 
 | } | 
 |  | 
 | // ClearAll clears out all existing entries from the pstore. This should be done | 
 | // after every start (after the relevant data has been read out) to ensure that | 
 | // there is always space to store new pstore entries and to minimize the risk | 
 | // of breaking badly-programmed firmware. | 
 | func ClearAll() error { | 
 | 	pstoreEntries, err := os.ReadDir(CanonicalMountPath) | 
 | 	if err != nil { | 
 | 		return fmt.Errorf("failed to list files in pstore: %w", err) | 
 | 	} | 
 | 	for _, entry := range pstoreEntries { | 
 | 		if err := os.Remove(filepath.Join(CanonicalMountPath, entry.Name())); err != nil { | 
 | 			return fmt.Errorf("failed to clear pstore entry: %w", err) | 
 | 		} | 
 | 	} | 
 | 	return nil | 
 | } |