1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.media.util;
18 
19 import static java.nio.file.StandardOpenOption.APPEND;
20 import static java.nio.file.StandardOpenOption.CREATE;
21 
22 import android.os.SystemProperties;
23 import android.text.format.DateUtils;
24 import android.util.Log;
25 
26 import androidx.annotation.GuardedBy;
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 
30 import java.io.File;
31 import java.io.IOException;
32 import java.io.PrintWriter;
33 import java.io.Writer;
34 import java.nio.file.Files;
35 import java.nio.file.Path;
36 import java.text.SimpleDateFormat;
37 import java.util.Date;
38 import java.util.Locale;
39 import java.util.stream.Stream;
40 
41 public class Logging {
42     public static final String TAG = "MediaProvider";
43     public static final boolean LOGW = Log.isLoggable(TAG, Log.WARN);
44     public static final boolean LOGD = Log.isLoggable(TAG, Log.DEBUG);
45     public static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE);
46 
47     public static final boolean IS_DEBUGGABLE =
48             SystemProperties.getInt("ro.debuggable", 0) == 1;
49 
50     /** Size limit of each persistent log file, in bytes */
51     private static final int PERSISTENT_SIZE = 32 * 1024;
52     private static final int PERSISTENT_COUNT = 4;
53     private static final long PERSISTENT_AGE = DateUtils.WEEK_IN_MILLIS;
54     private static final SimpleDateFormat DATE_FORMAT =
55             new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.ROOT);
56     private static final Object LOCK = new Object();
57 
58     @GuardedBy("LOCK")
59     private static Path sPersistentDir;
60     @GuardedBy("LOCK")
61     private static Path sPersistentFile;
62     @GuardedBy("LOCK")
63     private static Writer sWriter;
64 
65     /**
66      * Initialize persistent logging which is then available through
67      * {@link #logPersistent(String)} and {@link #dumpPersistent(PrintWriter)}.
68      */
initPersistent(@onNull File persistentDir)69     public static void initPersistent(@NonNull File persistentDir) {
70         synchronized (LOCK) {
71             sPersistentDir = persistentDir.toPath();
72             closeWriterAndUpdatePathLocked(null);
73         }
74     }
75 
76     /**
77      * Write the given message to persistent logs.
78      */
logPersistent(@onNull String format, @Nullable Object ... args)79     public static void logPersistent(@NonNull String format, @Nullable Object ... args) {
80         final String msg = (args == null || args.length == 0)
81                 ? format : String.format(Locale.ROOT, format, args);
82 
83         Log.i(TAG, msg);
84 
85         synchronized (LOCK) {
86             if (sPersistentDir == null) return;
87 
88             try {
89                 Path path = resolveCurrentPersistentFileLocked();
90                 if (!path.equals(sPersistentFile)) {
91                     closeWriterAndUpdatePathLocked(path);
92                 }
93 
94                 if (sWriter == null) {
95                     sWriter = Files.newBufferedWriter(path, CREATE, APPEND);
96                 }
97 
98                 sWriter.write(DATE_FORMAT.format(new Date()) + " " + msg + "\n");
99                 // Flush to guarantee that all our writes have been sent to the filesystem
100                 sWriter.flush();
101             } catch (IOException e) {
102                 closeWriterAndUpdatePathLocked(null);
103                 Log.w(TAG, "Failed to write: " + sPersistentFile, e);
104             }
105         }
106     }
107 
108     @GuardedBy("LOCK")
closeWriterAndUpdatePathLocked(@ullable Path newPath)109     private static void closeWriterAndUpdatePathLocked(@Nullable Path newPath) {
110         if (sWriter != null) {
111             try {
112                 sWriter.close();
113             } catch (IOException ignored) {
114                 Log.w(TAG, "Failed to close: " + sPersistentFile, ignored);
115             }
116             sWriter = null;
117         }
118         sPersistentFile = newPath;
119     }
120 
121     /**
122      * Trim any persistent logs, typically called during idle maintenance.
123      */
trimPersistent()124     public static void trimPersistent() {
125         File persistentDir = null;
126         synchronized (LOCK) {
127             if (sPersistentDir == null) return;
128             persistentDir = sPersistentDir.toFile();
129 
130             closeWriterAndUpdatePathLocked(sPersistentFile);
131         }
132 
133         FileUtils.deleteOlderFiles(persistentDir, PERSISTENT_COUNT, PERSISTENT_AGE);
134     }
135 
136     /**
137      * Dump any persistent logs.
138      */
dumpPersistent(@onNull PrintWriter pw)139     public static void dumpPersistent(@NonNull PrintWriter pw) {
140         Path persistentDir = null;
141         synchronized (LOCK) {
142             if (sPersistentDir == null) return;
143             persistentDir = sPersistentDir;
144         }
145 
146         try (Stream<Path> stream = Files.list(persistentDir)) {
147             stream.sorted().forEach((path) -> {
148                 dumpPersistentFile(path, pw);
149             });
150         } catch (IOException e) {
151             pw.println(e.getMessage());
152             pw.println();
153         }
154     }
155 
dumpPersistentFile(@onNull Path path, @NonNull PrintWriter pw)156     private static void dumpPersistentFile(@NonNull Path path, @NonNull PrintWriter pw) {
157         pw.println("Persistent logs in " + path + ":");
158         try (Stream<String> stream = Files.lines(path)) {
159             stream.forEach((line) -> {
160                 pw.println("  " + line);
161             });
162             pw.println();
163         } catch (IOException e) {
164             pw.println("  " + e.getMessage());
165             pw.println();
166         }
167     }
168 
169     /**
170      * Resolve the current log file to write new entries into. Automatically
171      * starts new files when the current file is larger than
172      * {@link #PERSISTENT_SIZE}.
173      */
174     @GuardedBy("LOCK")
resolveCurrentPersistentFileLocked()175     private static @NonNull Path resolveCurrentPersistentFileLocked() throws IOException {
176         if (sPersistentFile != null && sPersistentFile.toFile().length() < PERSISTENT_SIZE) {
177             return sPersistentFile;
178         }
179 
180         return sPersistentDir.resolve(String.valueOf(System.currentTimeMillis()));
181     }
182 }
183