1 /* 2 * Copyright (C) 2017 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.server.content; 18 19 import android.app.job.JobParameters; 20 import android.os.Build; 21 import android.os.Environment; 22 import android.os.FileUtils; 23 import android.os.SystemProperties; 24 import android.text.format.DateUtils; 25 import android.util.Log; 26 import android.util.Slog; 27 28 import com.android.internal.annotations.GuardedBy; 29 30 import libcore.io.IoUtils; 31 32 import java.io.BufferedReader; 33 import java.io.File; 34 import java.io.FileReader; 35 import java.io.FileWriter; 36 import java.io.IOException; 37 import java.io.PrintWriter; 38 import java.io.Reader; 39 import java.io.Writer; 40 import java.text.SimpleDateFormat; 41 import java.util.Arrays; 42 import java.util.Date; 43 import java.util.concurrent.TimeUnit; 44 45 /** 46 * Implements a rotating file logger for the sync manager, which is enabled only on userdebug/eng 47 * builds (unless debug.synclog is set to 1). 48 * 49 * Note this class could be used for other purposes too, but in general we don't want various 50 * system components to log to files, so it's put in a local package here. 51 */ 52 public class SyncLogger { 53 private static final String TAG = "SyncLogger"; 54 55 private static SyncLogger sInstance; 56 57 // Special UID used for logging to denote the self process. 58 public static final int CALLING_UID_SELF = -1; 59 60 SyncLogger() { 61 } 62 63 /** 64 * @return the singleton instance. 65 */ 66 public static synchronized SyncLogger getInstance() { 67 if (sInstance == null) { 68 final boolean enable = 69 Build.IS_DEBUGGABLE 70 || "1".equals(SystemProperties.get("debug.synclog")) 71 || Log.isLoggable(TAG, Log.VERBOSE); 72 if (enable) { 73 sInstance = new RotatingFileLogger(); 74 } else { 75 sInstance = new SyncLogger(); 76 } 77 } 78 return sInstance; 79 } 80 81 /** 82 * Write strings to the log file. 83 */ 84 public void log(Object... message) { 85 } 86 87 /** 88 * Remove old log files. 89 */ 90 public void purgeOldLogs() { 91 // The default implementation is no-op. 92 } 93 94 public String jobParametersToString(JobParameters params) { 95 // The default implementation is no-op. 96 return ""; 97 } 98 99 /** 100 * Dump all existing log files into a given writer. 101 */ 102 public void dumpAll(PrintWriter pw) { 103 } 104 105 /** 106 * @return whether log is enabled or not. 107 */ 108 public boolean enabled() { 109 return false; 110 } 111 112 /** 113 * Actual implementation which is only used on userdebug/eng builds (by default). 114 */ 115 private static class RotatingFileLogger extends SyncLogger { 116 private final Object mLock = new Object(); 117 118 private final long mKeepAgeMs = TimeUnit.DAYS.toMillis(7); 119 120 private static final SimpleDateFormat sTimestampFormat 121 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 122 123 private static final SimpleDateFormat sFilenameDateFormat 124 = new SimpleDateFormat("yyyy-MM-dd"); 125 126 @GuardedBy("mLock") 127 private final Date mCachedDate = new Date(); 128 129 @GuardedBy("mLock") 130 private final StringBuilder mStringBuilder = new StringBuilder(); 131 132 private final File mLogPath; 133 134 @GuardedBy("mLock") 135 private long mCurrentLogFileDayTimestamp; 136 137 @GuardedBy("mLock") 138 private Writer mLogWriter; 139 140 @GuardedBy("mLock") 141 private boolean mErrorShown; 142 143 private static final boolean DO_LOGCAT = Log.isLoggable(TAG, Log.DEBUG); 144 145 RotatingFileLogger() { 146 mLogPath = new File(Environment.getDataSystemDirectory(), "syncmanager-log"); 147 } 148 149 @Override 150 public boolean enabled() { 151 return true; 152 } 153 154 private void handleException(String message, Exception e) { 155 if (!mErrorShown) { 156 Slog.e(TAG, message, e); 157 mErrorShown = true; 158 } 159 } 160 161 @Override 162 public void log(Object... message) { 163 if (message == null) { 164 return; 165 } 166 synchronized (mLock) { 167 final long now = System.currentTimeMillis(); 168 openLogLocked(now); 169 if (mLogWriter == null) { 170 return; // Couldn't open log file? 171 } 172 173 mStringBuilder.setLength(0); 174 mCachedDate.setTime(now); 175 mStringBuilder.append(sTimestampFormat.format(mCachedDate)); 176 mStringBuilder.append(' '); 177 178 mStringBuilder.append(android.os.Process.myTid()); 179 mStringBuilder.append(' '); 180 181 final int messageStart = mStringBuilder.length(); 182 183 for (Object o : message) { 184 mStringBuilder.append(o); 185 } 186 mStringBuilder.append('\n'); 187 188 try { 189 mLogWriter.append(mStringBuilder); 190 mLogWriter.flush(); 191 192 // Also write on logcat. 193 if (DO_LOGCAT) { 194 Log.d(TAG, mStringBuilder.substring(messageStart)); 195 } 196 } catch (IOException e) { 197 handleException("Failed to write log", e); 198 } 199 } 200 } 201 202 @GuardedBy("mLock") 203 private void openLogLocked(long now) { 204 // If we already have a log file opened and the date has't changed, just use it. 205 final long day = now % DateUtils.DAY_IN_MILLIS; 206 if ((mLogWriter != null) && (day == mCurrentLogFileDayTimestamp)) { 207 return; 208 } 209 210 // Otherwise create a new log file. 211 closeCurrentLogLocked(); 212 213 mCurrentLogFileDayTimestamp = day; 214 215 mCachedDate.setTime(now); 216 final String filename = "synclog-" + sFilenameDateFormat.format(mCachedDate) + ".log"; 217 final File file = new File(mLogPath, filename); 218 219 file.getParentFile().mkdirs(); 220 221 try { 222 mLogWriter = new FileWriter(file, /* append= */ true); 223 } catch (IOException e) { 224 handleException("Failed to open log file: " + file, e); 225 } 226 } 227 228 @GuardedBy("mLock") 229 private void closeCurrentLogLocked() { 230 IoUtils.closeQuietly(mLogWriter); 231 mLogWriter = null; 232 } 233 234 @Override 235 public void purgeOldLogs() { 236 synchronized (mLock) { 237 FileUtils.deleteOlderFiles(mLogPath, /* keepCount= */ 1, mKeepAgeMs); 238 } 239 } 240 241 @Override 242 public String jobParametersToString(JobParameters params) { 243 return SyncJobService.jobParametersToString(params); 244 } 245 246 @Override 247 public void dumpAll(PrintWriter pw) { 248 synchronized (mLock) { 249 final String[] files = mLogPath.list(); 250 if (files == null || (files.length == 0)) { 251 return; 252 } 253 Arrays.sort(files); 254 255 for (String file : files) { 256 dumpFile(pw, new File(mLogPath, file)); 257 } 258 } 259 } 260 261 private void dumpFile(PrintWriter pw, File file) { 262 Slog.w(TAG, "Dumping " + file); 263 final char[] buffer = new char[32 * 1024]; 264 265 try (Reader in = new BufferedReader(new FileReader(file))) { 266 int read; 267 while ((read = in.read(buffer)) >= 0) { 268 if (read > 0) { 269 pw.write(buffer, 0, read); 270 } 271 } 272 } catch (IOException e) { 273 } 274 } 275 } 276 } 277