package main import ( "context" "os" "path/filepath" "slices" "sort" "strings" "google.golang.org/protobuf/types/known/emptypb" ) const ( powerReportsDirectoryName = "power_reports" powerTimelineFileName = "timeline.jsonl" powerEventsFileName = "events.jsonl" powerReportFileContentLimit = 1024 * 1024 ) // File order follows the client convention: // sing-box-for-apple Library/Shared/PowerReportManager.swift (availableFiles). var powerReportLeadingFileOrder = []string{metadataFileName, configSnapshotFileName, powerTimelineFileName, powerEventsFileName, goLogFileName} func (s *desktopService) ListPowerReports(ctx context.Context, empty *emptypb.Empty) (*OOMReportList, error) { reportsDirectory, userID, err := s.daemon.reportCaller(ctx, powerReportsDirectoryName) if err != nil { return nil, err } entries, err := os.ReadDir(reportsDirectory) if err != nil { if os.IsNotExist(err) { return &OOMReportList{}, nil } return nil, err } reports := make([]*OOMReportEntry, 0, len(entries)) for _, entry := range entries { if !entry.IsDir() { continue } fullPath := filepath.Join(reportsDirectory, entry.Name()) if !reportOwnedBy(fullPath, userID) { continue } reports = append(reports, &OOMReportEntry{ Name: entry.Name(), RecordedAt: reportTime(fullPath, "startedAt").UnixMilli(), IsRead: reportIsRead(fullPath), }) } sort.Slice(reports, func(i, j int) bool { return reports[i].RecordedAt > reports[j].RecordedAt }) return &OOMReportList{Reports: reports}, nil } func (s *desktopService) ReadPowerReport(ctx context.Context, request *OOMReportRequest) (*OOMReportContent, error) { reportsDirectory, userID, err := s.daemon.reportCaller(ctx, powerReportsDirectoryName) if err != nil { return nil, err } fullPath, err := reportPathForUser(reportsDirectory, request.Name, userID) if err != nil { return nil, err } files := make([]*OOMReportFile, 0, len(powerReportLeadingFileOrder)) for _, fileName := range powerReportLeadingFileOrder { content, readError := readFileTail(filepath.Join(fullPath, fileName), powerReportFileContentLimit) if readError != nil { if os.IsNotExist(readError) { continue } return nil, readError } files = append(files, &OOMReportFile{ Name: fileName, Content: content, }) } entries, err := os.ReadDir(fullPath) if err != nil { return nil, err } profileNames := make([]string, 0, len(entries)) for _, entry := range entries { name := entry.Name() if entry.IsDir() || strings.HasPrefix(name, ".") { continue } if slices.Contains(powerReportLeadingFileOrder, name) { continue } profileNames = append(profileNames, name) } sort.Strings(profileNames) for _, name := range profileNames { files = append(files, &OOMReportFile{ Name: name, IsProfile: true, }) } return &OOMReportContent{Files: files}, nil } func readFileTail(path string, limit int64) ([]byte, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() info, err := file.Stat() if err != nil { return nil, err } if info.Size() <= limit { return os.ReadFile(path) } _, err = file.Seek(info.Size()-limit, 0) if err != nil { return nil, err } content := make([]byte, 0, limit+4) content = append(content, "…\n"...) buffer := make([]byte, limit) n, err := file.Read(buffer) if err != nil { return nil, err } return append(content, buffer[:n]...), nil } func (s *desktopService) MarkPowerReportRead(ctx context.Context, request *OOMReportRequest) (*emptypb.Empty, error) { reportsDirectory, userID, err := s.daemon.reportCaller(ctx, powerReportsDirectoryName) if err != nil { return nil, err } fullPath, err := reportPathForUser(reportsDirectory, request.Name, userID) if err != nil { return nil, err } err = os.WriteFile(filepath.Join(fullPath, readMarkerFileName), nil, 0o600) if err != nil { return nil, err } return &emptypb.Empty{}, nil } func (s *desktopService) ExportPowerReport(ctx context.Context, request *OOMReportExportRequest) (*CrashReportArchive, error) { reportsDirectory, userID, err := s.daemon.reportCaller(ctx, powerReportsDirectoryName) if err != nil { return nil, err } return exportReportArchive(reportsDirectory, request.Name, userID, request.WithConfiguration, request.WithLog, request.Encrypt) } func (s *desktopService) DeletePowerReport(ctx context.Context, request *OOMReportRequest) (*emptypb.Empty, error) { reportsDirectory, userID, err := s.daemon.reportCaller(ctx, powerReportsDirectoryName) if err != nil { return nil, err } fullPath, err := reportPathForUser(reportsDirectory, request.Name, userID) if err != nil { return nil, err } err = os.RemoveAll(fullPath) if err != nil { return nil, err } return &emptypb.Empty{}, nil } func (s *desktopService) DeleteAllPowerReports(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { reportsDirectory, userID, err := s.daemon.reportCaller(ctx, powerReportsDirectoryName) if err != nil { return nil, err } err = deleteReportsForUser(reportsDirectory, userID) if err != nil { return nil, err } return &emptypb.Empty{}, nil }