1 /*******************************************************************************
2  * Copyright (c) 2009, 2017 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.osgi.internal.debug;
15 
16 import java.io.BufferedWriter;
17 import java.io.ByteArrayOutputStream;
18 import java.io.File;
19 import java.io.FilterOutputStream;
20 import java.io.IOException;
21 import java.io.OutputStream;
22 import java.io.OutputStreamWriter;
23 import java.io.PrintStream;
24 import java.io.Writer;
25 import java.nio.charset.StandardCharsets;
26 import java.security.AccessController;
27 import java.text.MessageFormat;
28 import java.text.SimpleDateFormat;
29 import java.util.Date;
30 import org.eclipse.core.runtime.adaptor.EclipseStarter;
31 import org.eclipse.osgi.framework.util.SecureAction;
32 import org.eclipse.osgi.service.debug.DebugTrace;
33 
34 /**
35  * The DebugTrace implementation for Eclipse.
36  */
37 class EclipseDebugTrace implements DebugTrace {
38 
39 	/** The system property used to specify size a trace file can grow before it is rotated */
40 	private static final String PROP_TRACE_SIZE_MAX = "eclipse.trace.size.max"; //$NON-NLS-1$
41 	/** The system property used to specify the maximum number of backup trace files to use */
42 	private static final String PROP_TRACE_FILE_MAX = "eclipse.trace.backup.max"; //$NON-NLS-1$
43 	/** The trace message for a thread stack dump */
44 	private final static String MESSAGE_THREAD_DUMP = "Thread Stack dump: "; //$NON-NLS-1$
45 	/** The trace message for a method completing with a return value */
46 	private final static String MESSAGE_EXIT_METHOD_WITH_RESULTS = "Exiting method {0}with result: "; //$NON-NLS-1$
47 	/** The trace message for a method completing with no return value */
48 	private final static String MESSAGE_EXIT_METHOD_NO_RESULTS = "Exiting method {0}with a void return"; //$NON-NLS-1$
49 	/** The trace message for a method starting with a set of arguments */
50 	private final static String MESSAGE_ENTER_METHOD_WITH_PARAMS = "Entering method {0}with parameters: ("; //$NON-NLS-1$
51 	/** The trace message for a method starting with no arguments */
52 	private final static String MESSAGE_ENTER_METHOD_NO_PARAMS = "Entering method {0}with no parameters"; //$NON-NLS-1$
53 	/** The version attribute written in the header of a new session */
54 	private final static String TRACE_FILE_VERSION_COMMENT = "version: "; //$NON-NLS-1$
55 	/** The verbose attribute written in the header of a new session */
56 	private final static String TRACE_FILE_VERBOSE_COMMENT = "verbose: "; //$NON-NLS-1$
57 	/** The version value written in the header of a new session */
58 	private final static String TRACE_FILE_VERSION = "1.1"; //$NON-NLS-1$
59 	/** The new session identifier to be written whenever a new session starts */
60 	private final static String TRACE_NEW_SESSION = "!SESSION "; //$NON-NLS-1$
61 	/** The date attribute written to the header of the trace file to show when this file was created */
62 	private final static String TRACE_FILE_DATE = "Time of creation: "; //$NON-NLS-1$
63 	/** Trace date formatter using the pattern: yyyy-MM-dd HH:mm:ss.SSS  */
64 	private final static SimpleDateFormat TRACE_FILE_DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); //$NON-NLS-1$
65 	/** The comment character used by the trace file */
66 	private final static String TRACE_COMMENT = "#"; //$NON-NLS-1$
67 	/** The delimiter used to separate trace elements such as the time stamp, message, etc */
68 	private final static String TRACE_ELEMENT_DELIMITER = "|"; //$NON-NLS-1$
69 	/** The string written in place of the {@link EclipseDebugTrace#TRACE_TRACE_ELEMENT_DELIMITER} in entries */
70 	private final static String TRACE_ELEMENT_DELIMITER_ENCODED = "|"; //$NON-NLS-1$
71 	/** OS-specific line separator */
72 	private static final String LINE_SEPARATOR;
73 	static {
74 		String s = System.getProperty("line.separator"); //$NON-NLS-1$
75 		LINE_SEPARATOR = s == null ? "\n" : s; //$NON-NLS-1$
76 	}
77 	/** The value written to the trace file if a null object is being traced */
78 	private final static String NULL_VALUE = "<null>"; //$NON-NLS-1$
79 	/**  */
80 	private final static SecureAction secureAction = AccessController.doPrivileged(SecureAction.createSecureAction());
81 
82 	/******************* Tracing file attributes **************************/
83 	/** The default size a trace file can grow before it is rotated */
84 	private static final int DEFAULT_TRACE_FILE_SIZE = 1000; // The value is in KB.
85 	/** The default number of backup trace files */
86 	private static final int DEFAULT_TRACE_FILES = 10;
87 	/** The minimum size limit for trace file rotation */
88 	private static final int DEFAULT_TRACE_FILE_MIN_SIZE = 10;
89 	/** The extension used for log files */
90 	private static final String TRACE_FILE_EXTENSION = ".trace"; //$NON-NLS-1$
91 	/** The extension markup to use for backup log files*/
92 	private static final String BACKUP_MARK = ".bak_"; //$NON-NLS-1$
93 	/** The maximum size that a trace file should grow (0 = unlimited) */
94 	private int maxTraceFileSize = DEFAULT_TRACE_FILE_SIZE; // The value is in KB.
95 	/** The maximum number of trace files that should be saved */
96 	private int maxTraceFiles = DEFAULT_TRACE_FILES;
97 	/** The index of the currently backed-up trace file */
98 	private int backupTraceFileIndex = 0;
99 
100 	/** An optional argument to specify the name of the class used by clients to trace messages.  If no trace class is specified
101 	 * then the class calling this API is assumed to be the class being traced.
102 	*/
103 	private String traceClass = null;
104 	/** The symbolic name of the bundle being traced */
105 	private String bundleSymbolicName = null;
106 	/** DebugOptions are used to determine if the specified bundle symbolic name + option-path has debugging enabled */
107 	private FrameworkDebugOptions debugOptions = null;
108 
109 	private final boolean consoleLog;
110 
111 	/**
112 	 * Construct a new EclipseDebugTrace for the specified bundle symbolic name and write messages to the specified
113 	 * trace file.
114 	 *
115 	 * @param bundleSymbolicName The symbolic name of the bundle being traced
116 	 * @param debugOptions Used to determine if the specified bundle symbolic name + option-path has tracing enabled
117 	 * @param traceClass The class that the client is using to perform trace API calls
118 	 */
EclipseDebugTrace(final String bundleSymbolicName, final FrameworkDebugOptions debugOptions, final Class<?> traceClass)119 	EclipseDebugTrace(final String bundleSymbolicName, final FrameworkDebugOptions debugOptions, final Class<?> traceClass) {
120 		this.consoleLog = "true".equals(debugOptions.getConfiguration().getConfiguration(EclipseStarter.PROP_CONSOLE_LOG)); //$NON-NLS-1$
121 		this.traceClass = traceClass != null ? traceClass.getName() : null;
122 		this.debugOptions = debugOptions;
123 		this.bundleSymbolicName = bundleSymbolicName;
124 		readLogProperties();
125 	}
126 
127 	/**
128 	 * Is debugging enabled for the specified option-path
129 	 *
130 	 * @param optionPath The <i>option-path</i>
131 	 * @return Returns true if debugging is enabled for the specified option-path on this bundle; Otherwise false.
132 	 */
isDebuggingEnabled(final String optionPath)133 	private final boolean isDebuggingEnabled(final String optionPath) {
134 		if (optionPath == null)
135 			return true;
136 		boolean debugEnabled = false;
137 		if (debugOptions.isDebugEnabled()) {
138 			final String option = bundleSymbolicName + optionPath;
139 			debugEnabled = debugOptions.getBooleanOption(option, false);
140 		}
141 		return debugEnabled;
142 	}
143 
144 	/*
145 	 * (non-Javadoc)
146 	 * @see org.eclipse.osgi.framework.debug.FrameworkDebugTrace#trace(java.lang.String, java.lang.String)
147 	 */
148 	@Override
trace(final String optionPath, final String message)149 	public void trace(final String optionPath, final String message) {
150 
151 		if (isDebuggingEnabled(optionPath)) {
152 			final FrameworkDebugTraceEntry record = new FrameworkDebugTraceEntry(bundleSymbolicName, optionPath, message, traceClass);
153 			writeRecord(record);
154 		}
155 	}
156 
157 	/*
158 	 * (non-Javadoc)
159 	 * @see org.eclipse.osgi.framework.debug.FrameworkDebugTrace#trace(java.lang.String, java.lang.String, java.lang.Throwable)
160 	 */
161 	@Override
trace(final String optionPath, final String message, final Throwable error)162 	public void trace(final String optionPath, final String message, final Throwable error) {
163 
164 		if (isDebuggingEnabled(optionPath)) {
165 			final FrameworkDebugTraceEntry record = new FrameworkDebugTraceEntry(bundleSymbolicName, optionPath, message, error, traceClass);
166 			writeRecord(record);
167 		}
168 	}
169 
170 	/*
171 	 * (non-Javadoc)
172 	 * @see org.eclipse.osgi.framework.debug.FrameworkDebugTrace#traceEntry(java.lang.String)
173 	 */
174 	@Override
traceEntry(final String optionPath)175 	public void traceEntry(final String optionPath) {
176 
177 		if (isDebuggingEnabled(optionPath)) {
178 			final FrameworkDebugTraceEntry record = new FrameworkDebugTraceEntry(bundleSymbolicName, optionPath, null, traceClass);
179 			setMessage(record, EclipseDebugTrace.MESSAGE_ENTER_METHOD_NO_PARAMS);
180 			writeRecord(record);
181 		}
182 	}
183 
184 	/*
185 	 * (non-Javadoc)
186 	 * @see org.eclipse.osgi.framework.debug.FrameworkDebugTrace#traceEntry(java.lang.String, java.lang.Object)
187 	 */
188 	@Override
traceEntry(final String optionPath, final Object methodArgument)189 	public void traceEntry(final String optionPath, final Object methodArgument) {
190 
191 		if (isDebuggingEnabled(optionPath)) {
192 			traceEntry(optionPath, new Object[] {methodArgument});
193 		}
194 	}
195 
196 	/*
197 	 * (non-Javadoc)
198 	 * @see org.eclipse.osgi.framework.debug.FrameworkDebugTrace#traceEntry(java.lang.String, java.lang.Object[])
199 	 */
200 	@Override
traceEntry(final String optionPath, final Object[] methodArguments)201 	public void traceEntry(final String optionPath, final Object[] methodArguments) {
202 
203 		if (isDebuggingEnabled(optionPath)) {
204 			final StringBuilder messageBuffer = new StringBuilder(EclipseDebugTrace.MESSAGE_ENTER_METHOD_WITH_PARAMS);
205 			if (methodArguments != null) {
206 				int i = 0;
207 				while (i < methodArguments.length) {
208 					if (methodArguments[i] != null) {
209 						messageBuffer.append(methodArguments[i].toString());
210 					} else {
211 						messageBuffer.append(EclipseDebugTrace.NULL_VALUE);
212 					}
213 					i++;
214 					if (i < methodArguments.length) {
215 						messageBuffer.append(" "); //$NON-NLS-1$
216 					}
217 				}
218 				messageBuffer.append(")"); //$NON-NLS-1$
219 			}
220 			final FrameworkDebugTraceEntry record = new FrameworkDebugTraceEntry(bundleSymbolicName, optionPath, null, traceClass);
221 			setMessage(record, messageBuffer.toString());
222 			writeRecord(record);
223 		}
224 	}
225 
226 	/*
227 	 * (non-Javadoc)
228 	 * @see org.eclipse.osgi.framework.debug.FrameworkDebugTrace#traceExit(java.lang.String)
229 	 */
230 	@Override
traceExit(final String optionPath)231 	public void traceExit(final String optionPath) {
232 
233 		if (isDebuggingEnabled(optionPath)) {
234 			final FrameworkDebugTraceEntry record = new FrameworkDebugTraceEntry(bundleSymbolicName, optionPath, null, traceClass);
235 			setMessage(record, EclipseDebugTrace.MESSAGE_EXIT_METHOD_NO_RESULTS);
236 			writeRecord(record);
237 		}
238 	}
239 
240 	/*
241 	 * (non-Javadoc)
242 	 * @see org.eclipse.osgi.framework.debug.FrameworkDebugTrace#traceExit(java.lang.String, java.lang.Object)
243 	 */
244 	@Override
traceExit(final String optionPath, final Object result)245 	public void traceExit(final String optionPath, final Object result) {
246 
247 		if (isDebuggingEnabled(optionPath)) {
248 			final StringBuilder messageBuffer = new StringBuilder(EclipseDebugTrace.MESSAGE_EXIT_METHOD_WITH_RESULTS);
249 			if (result == null) {
250 				messageBuffer.append(EclipseDebugTrace.NULL_VALUE);
251 			} else {
252 				messageBuffer.append(result.toString());
253 			}
254 			final FrameworkDebugTraceEntry record = new FrameworkDebugTraceEntry(bundleSymbolicName, optionPath, null, traceClass);
255 			setMessage(record, messageBuffer.toString());
256 			writeRecord(record);
257 		}
258 	}
259 
260 	/*
261 	 * (non-Javadoc)
262 	 * @see org.eclipse.osgi.framework.debug.FrameworkDebugTrace#traceDumpStack(java.lang.String)
263 	 */
264 	@Override
traceDumpStack(final String optionPath)265 	public void traceDumpStack(final String optionPath) {
266 
267 		if (isDebuggingEnabled(optionPath)) {
268 			final StringBuilder messageBuffer = new StringBuilder(EclipseDebugTrace.MESSAGE_THREAD_DUMP);
269 			StackTraceElement[] elements = new Exception().getStackTrace();
270 			// the first element in this stack trace is going to be this class, so ignore it
271 			// the second element in this stack trace is going to either be the caller or the trace class.  Ignore it only if a traceClass is defined
272 			// the rest of the elements should be included in the file array
273 			int firstIndex = (traceClass == null) ? 1 : 2;
274 			int endIndex = elements.length - firstIndex;
275 			final StackTraceElement[] newElements = new StackTraceElement[endIndex];
276 			int i = 0;
277 			while (i < endIndex) {
278 				newElements[i] = elements[firstIndex];
279 				i++;
280 				firstIndex++;
281 			}
282 			messageBuffer.append(convertStackTraceElementsToString(newElements));
283 			final FrameworkDebugTraceEntry record = new FrameworkDebugTraceEntry(bundleSymbolicName, optionPath, messageBuffer.toString(), traceClass);
284 			writeRecord(record);
285 		}
286 	}
287 
288 	/**
289 	 * Set the trace message for the specified record to include class and method information
290 	 * if verbose debugging is disabled.
291 	 *
292 	 * @param record The {@link FrameworkDebugTraceEntry} containing the information to persist to the trace file.
293 	 * @param originalMessage The original tracing message
294 	 */
setMessage(final FrameworkDebugTraceEntry record, final String originalMessage)295 	private final void setMessage(final FrameworkDebugTraceEntry record, final String originalMessage) {
296 
297 		String argument = null;
298 		if (!debugOptions.isVerbose()) {
299 			final StringBuilder classMethodName = new StringBuilder(record.getClassName());
300 			classMethodName.append("#"); //$NON-NLS-1$
301 			classMethodName.append(record.getMethodName());
302 			classMethodName.append(" "); //$NON-NLS-1$
303 			argument = classMethodName.toString();
304 		} else {
305 			argument = ""; //$NON-NLS-1$
306 		}
307 		String newMessage = MessageFormat.format(originalMessage, new Object[] {argument});
308 		record.setMessage(newMessage);
309 	}
310 
311 	/**
312 	 * Utility method to convert an array of StackTraceElement objects to form a String representation of a stack dump
313 	 *
314 	 * @param elements
315 	 *            The array of StackTraceElement objects
316 	 * @return A String of the stack dump produced by the list of elements
317 	 */
convertStackTraceElementsToString(final StackTraceElement[] elements)318 	private final String convertStackTraceElementsToString(final StackTraceElement[] elements) {
319 
320 		final StringBuilder buffer = new StringBuilder();
321 		if (elements != null) {
322 			buffer.append("java.lang.Throwable: "); //$NON-NLS-1$
323 			buffer.append(EclipseDebugTrace.LINE_SEPARATOR);
324 			int i = 0;
325 			while (i < elements.length) {
326 				if (elements[i] != null) {
327 					buffer.append("\tat "); //$NON-NLS-1$
328 					buffer.append(elements[i].toString());
329 					buffer.append(EclipseDebugTrace.LINE_SEPARATOR);
330 				}
331 				i++;
332 			}
333 		}
334 		return buffer.toString();
335 	}
336 
337 	/**
338 	 * Write the specified FrameworkTraceEntry to trace file
339 	 *
340 	 * @param entry The FrameworkTraceEntry to write to the log file.
341 	 */
writeRecord(final FrameworkDebugTraceEntry entry)342 	private void writeRecord(final FrameworkDebugTraceEntry entry) {
343 
344 		if (entry != null) {
345 			synchronized (debugOptions.getWriteLock()) {
346 				final File tracingFile = debugOptions.getFile(); // the tracing file may be null if it has not been set
347 				Writer traceWriter = null;
348 				try {
349 					// check to see if the file should be rotated
350 					checkTraceFileSize(tracingFile, entry.getTimestamp());
351 					// open the trace file
352 					traceWriter = openWriter(tracingFile);
353 					if (debugOptions.newSession()) {
354 						writeSession(traceWriter, entry.getTimestamp());
355 					}
356 					writeMessage(traceWriter, entry);
357 					// flush the writer
358 					traceWriter.flush();
359 				} catch (Exception ex) {
360 					// any exceptions during tracing should be caught
361 					System.err.println("An exception occurred while writing to the platform trace file: ");//$NON-NLS-1$
362 					ex.printStackTrace(System.err);
363 				} finally {
364 					// close the trace writer
365 					closeWriter(traceWriter);
366 				}
367 			}
368 		}
369 	}
370 
371 	/**
372 	 * Reads the PROP_TRACE_SIZE_MAX and PROP_TRACE_FILE_MAX properties.
373 	 */
readLogProperties()374 	private void readLogProperties() {
375 
376 		String newMaxTraceFileSize = debugOptions.getConfiguration().getConfiguration(PROP_TRACE_SIZE_MAX);
377 		if (newMaxTraceFileSize != null) {
378 			maxTraceFileSize = Integer.parseInt(newMaxTraceFileSize);
379 			if (maxTraceFileSize != 0 && maxTraceFileSize < DEFAULT_TRACE_FILE_MIN_SIZE) {
380 				// If the value is '0', then it means no size limitation.
381 				// Also, make sure no inappropriate(too small) assigned value.
382 				maxTraceFileSize = DEFAULT_TRACE_FILE_MIN_SIZE;
383 			}
384 		}
385 
386 		String newMaxLogFiles = debugOptions.getConfiguration().getConfiguration(PROP_TRACE_FILE_MAX);
387 		if (newMaxLogFiles != null) {
388 			maxTraceFiles = Integer.parseInt(newMaxLogFiles);
389 			if (maxTraceFiles < 1) {
390 				// Make sure no invalid assigned value. (at least >= 1)
391 				maxTraceFiles = DEFAULT_TRACE_FILES;
392 			}
393 		}
394 	}
395 
396 	/**
397 	 * Checks the trace file size.  If the file size reaches the limit then the trace file is rotated.
398 	 *
399 	 * @param traceFile The tracing file
400 	 * @param timestamp the timestamp for the session; this is the same timestamp as the first entry
401 	 * @return false if an error occurred trying to rotate the trace file
402 	 */
checkTraceFileSize(final File traceFile, long timestamp)403 	private boolean checkTraceFileSize(final File traceFile, long timestamp) {
404 
405 		// 0 file size means there is no size limit
406 		boolean isBackupOK = true;
407 		if (maxTraceFileSize > 0) {
408 			if ((traceFile != null) && traceFile.exists()) {
409 				if ((traceFile.length() >> 10) > maxTraceFileSize) { // Use KB as file size unit.
410 					final String traceFileName = traceFile.getAbsolutePath();
411 
412 					// Delete old backup file that will be replaced.
413 					String backupFilename = ""; //$NON-NLS-1$
414 					if (traceFileName.toLowerCase().endsWith(TRACE_FILE_EXTENSION)) {
415 						backupFilename = traceFileName.substring(0, traceFileName.length() - TRACE_FILE_EXTENSION.length()) + BACKUP_MARK + backupTraceFileIndex + TRACE_FILE_EXTENSION;
416 					} else {
417 						backupFilename = traceFileName + BACKUP_MARK + backupTraceFileIndex;
418 					}
419 					final File backupFile = new File(backupFilename);
420 					if (backupFile.exists()) {
421 						if (!backupFile.delete()) {
422 							System.err.println("Error when trying to delete old trace file: " + backupFile.getName());//$NON-NLS-1$
423 							if (backupFile.renameTo(new File(backupFile.getAbsolutePath() + System.currentTimeMillis()))) {
424 								System.err.println("So we rename it to filename: " + backupFile.getName()); //$NON-NLS-1$
425 							} else {
426 								System.err.println("And we also cannot rename it!"); //$NON-NLS-1$
427 								isBackupOK = false;
428 							}
429 						}
430 					}
431 
432 					// Rename current log file to backup one.
433 					boolean isRenameOK = traceFile.renameTo(backupFile);
434 					if (!isRenameOK) {
435 						System.err.println("Error when trying to rename trace file to backup one."); //$NON-NLS-1$
436 						isBackupOK = false;
437 					}
438 					/*
439 					 * Write a header to new log file stating that this new file is a continuation file.
440 					 * This method should already be called with the file lock set so we should be safe
441 					 * to update it here.
442 					*/
443 					Writer traceWriter = null;
444 					try {
445 						traceWriter = openWriter(traceFile);
446 						writeComment(traceWriter, "This is a continuation of trace file " + backupFile.getAbsolutePath()); //$NON-NLS-1$
447 						writeComment(traceWriter, EclipseDebugTrace.TRACE_FILE_VERSION_COMMENT + EclipseDebugTrace.TRACE_FILE_VERSION);
448 						writeComment(traceWriter, EclipseDebugTrace.TRACE_FILE_VERBOSE_COMMENT + debugOptions.isVerbose());
449 						writeComment(traceWriter, EclipseDebugTrace.TRACE_FILE_DATE + getFormattedDate(timestamp));
450 						traceWriter.flush();
451 					} catch (IOException ioEx) {
452 						ioEx.printStackTrace();
453 					} finally {
454 						closeWriter(traceWriter);
455 					}
456 					backupTraceFileIndex = (++backupTraceFileIndex) % maxTraceFiles;
457 				}
458 			}
459 		}
460 		return isBackupOK;
461 	}
462 
463 	/**
464 	 * Writes a comment to the trace file
465 	 *
466 	 * @param traceWriter the trace writer
467 	 * @param comment the comment to be written to the trace file
468 	 * @throws IOException If an error occurs while writing the comment
469 	 */
writeComment(final Writer traceWriter, final String comment)470 	private void writeComment(final Writer traceWriter, final String comment) throws IOException {
471 
472 		StringBuilder commentText = new StringBuilder(EclipseDebugTrace.TRACE_COMMENT);
473 		commentText.append(" "); //$NON-NLS-1$
474 		commentText.append(comment);
475 		commentText.append(EclipseDebugTrace.LINE_SEPARATOR);
476 		traceWriter.write(commentText.toString());
477 	}
478 
479 	/**
480 	 * Accessor to retrieve the time stamp in a formatted manner.
481 	 *
482 	 * @return A formatted time stamp based on the {@link EclipseDebugTrace#TRACE_FILE_DATE_FORMATTER} formatter
483 	 */
getFormattedDate(long timestamp)484 	private final String getFormattedDate(long timestamp) {
485 
486 		return EclipseDebugTrace.TRACE_FILE_DATE_FORMATTER.format(new Date(timestamp));
487 	}
488 
489 	/**
490 	 * Accessor to retrieve the text of a {@link Throwable} in a formatted manner so that it can be written to the
491 	 * trace file.
492 	 *
493 	 * @param error The {@lnk Throwable} to format
494 	 * @return The complete text of a {@link Throwable} as a {@link String} or null if the input error is null.
495 	 */
getFormattedThrowable(Throwable error)496 	private final String getFormattedThrowable(Throwable error) {
497 
498 		String result = null;
499 		if (error != null) {
500 			PrintStream throwableStream = null;
501 			try {
502 				ByteArrayOutputStream throwableByteOutputStream = new ByteArrayOutputStream();
503 				throwableStream = new PrintStream(throwableByteOutputStream, false);
504 				error.printStackTrace(throwableStream);
505 				result = encodeText(throwableByteOutputStream.toString());
506 			} finally {
507 				if (throwableStream != null) {
508 					throwableStream.close();
509 				}
510 			}
511 		}
512 		return result;
513 	}
514 
515 	/**
516 	 * Writes header information to a new trace file
517 	 *
518 	 * @param traceWriter the trace writer
519 	 * @param timestamp the timestamp for the session; this is the same timestamp as the first entry
520 	 * @throws IOException If an error occurs while writing this session information
521 	 */
writeSession(final Writer traceWriter, long timestamp)522 	private void writeSession(final Writer traceWriter, long timestamp) throws IOException {
523 
524 		writeComment(traceWriter, EclipseDebugTrace.TRACE_NEW_SESSION + this.getFormattedDate(timestamp));
525 		writeComment(traceWriter, EclipseDebugTrace.TRACE_FILE_VERSION_COMMENT + EclipseDebugTrace.TRACE_FILE_VERSION);
526 		writeComment(traceWriter, EclipseDebugTrace.TRACE_FILE_VERBOSE_COMMENT + debugOptions.isVerbose());
527 		writeComment(traceWriter, "The following option strings are specified for this debug session:"); //$NON-NLS-1$
528 		final String[] allOptions = debugOptions.getAllOptions();
529 		for (String allOption : allOptions) {
530 			writeComment(traceWriter, "\t" + allOption); //$NON-NLS-1$
531 		}
532 	}
533 
534 	/**
535 	 * Writes the specified trace entry object to the trace file using the
536 	 * {@link EclipseDebugTrace#TRACE_ELEMENT_DELIMITER} as the delimiter between
537 	 * each element of the entry.
538 	 *
539 	 * @param traceWriter the trace writer
540 	 * @param entry The trace entry object to write to the trace file
541 	 * @throws IOException If an error occurs while writing this message
542 	 */
writeMessage(final Writer traceWriter, final FrameworkDebugTraceEntry entry)543 	private void writeMessage(final Writer traceWriter, final FrameworkDebugTraceEntry entry) throws IOException {
544 
545 		final StringBuilder message = new StringBuilder(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
546 		message.append(" "); //$NON-NLS-1$
547 		message.append(encodeText(entry.getThreadName()));
548 		message.append(" "); //$NON-NLS-1$
549 		message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
550 		message.append(" "); //$NON-NLS-1$
551 		message.append(this.getFormattedDate(entry.getTimestamp()));
552 		message.append(" "); //$NON-NLS-1$
553 		message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
554 		message.append(" "); //$NON-NLS-1$
555 		if (!debugOptions.isVerbose()) {
556 			// format the trace entry for quiet tracing: only the thread name, timestamp, trace message, and exception (if necessary)
557 			message.append(encodeText(entry.getMessage()));
558 		} else {
559 			// format the trace entry for verbose tracing
560 			message.append(entry.getBundleSymbolicName());
561 			message.append(" "); //$NON-NLS-1$
562 			message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
563 			message.append(" "); //$NON-NLS-1$
564 			message.append(encodeText(entry.getOptionPath()));
565 			message.append(" "); //$NON-NLS-1$
566 			message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
567 			message.append(" "); //$NON-NLS-1$
568 			message.append(entry.getClassName());
569 			message.append(" "); //$NON-NLS-1$
570 			message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
571 			message.append(" "); //$NON-NLS-1$
572 			message.append(entry.getMethodName());
573 			message.append(" "); //$NON-NLS-1$
574 			message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
575 			message.append(" "); //$NON-NLS-1$
576 			message.append(entry.getLineNumber());
577 			message.append(" "); //$NON-NLS-1$
578 			message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
579 			message.append(" "); //$NON-NLS-1$
580 			message.append(encodeText(entry.getMessage()));
581 		}
582 		if (entry.getThrowable() != null) {
583 			message.append(" "); //$NON-NLS-1$
584 			message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
585 			message.append(" "); //$NON-NLS-1$
586 			message.append(this.getFormattedThrowable(entry.getThrowable()));
587 		}
588 		message.append(" "); //$NON-NLS-1$
589 		message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
590 		message.append(EclipseDebugTrace.LINE_SEPARATOR);
591 		// write the message
592 		if ((traceWriter != null) && (message != null)) {
593 			traceWriter.write(message.toString());
594 		}
595 	}
596 
597 	/**
598 	 * Encodes the specified string to replace any occurrence of the {@link EclipseDebugTrace#TRACE_ELEMENT_DELIMITER}
599 	 * string with the {@link EclipseDebugTrace#TRACE_ELEMENT_DELIMITER_ENCODED}
600 	 * string.  This can be used to ensure that the delimiter character does not break parsing when
601 	 * the entry text contains the delimiter character.
602 	 *
603 	 * @param inputString The original string to be written to the trace file.
604 	 * @return The original input string with all occurrences of
605 	 * {@link EclipseDebugTrace#TRACE_ELEMENT_DELIMITER} replaced with
606 	 * {@link EclipseDebugTrace#TRACE_ELEMENT_DELIMITER_ENCODED}. A <code>null</code> value will be
607 	 * returned if the input string is <code>null</code>.
608 	 */
encodeText(final String inputString)609 	private static String encodeText(final String inputString) {
610 		if (inputString == null || inputString.indexOf(TRACE_ELEMENT_DELIMITER) < 0)
611 			return inputString;
612 		final StringBuilder tempBuffer = new StringBuilder(inputString);
613 		int currentIndex = tempBuffer.indexOf(TRACE_ELEMENT_DELIMITER);
614 		while (currentIndex >= 0) {
615 			tempBuffer.replace(currentIndex, currentIndex + TRACE_ELEMENT_DELIMITER.length(), TRACE_ELEMENT_DELIMITER_ENCODED);
616 			currentIndex = tempBuffer.indexOf(TRACE_ELEMENT_DELIMITER);
617 		}
618 		return tempBuffer.toString();
619 	}
620 
621 	/**
622 	 * Returns a Writer for the given OutputStream
623 	 * @param output an OutputStream to use for the Writer
624 	 * @return A Writer for the given OutputStream
625 	 */
logForStream(OutputStream output)626 	private Writer logForStream(OutputStream output) {
627 		return new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
628 	}
629 
630 	/**
631 	 * Creates the trace writer.
632 	 * If the tracing file is null then the writer will use System.out to print any messages.
633 	 *
634 	 * @param traceFile The tracing file
635 	 * @return Returns a new Writer object
636 	 */
openWriter(final File traceFile)637 	private Writer openWriter(final File traceFile) {
638 		OutputStream out = null;
639 		if (traceFile != null) {
640 			try {
641 				out = secureAction.getFileOutputStream(traceFile, true);
642 			} catch (IOException ioEx) {
643 				// ignore and fall back to system.out; but print error message to indicate what happened
644 				System.err.println("Unable to open trace file: " + traceFile + ": " + ioEx.getMessage()); //$NON-NLS-1$ //$NON-NLS-2$
645 			}
646 		}
647 		if (out == null) {
648 			out = new FilterOutputStream(System.out) {
649 				/**
650 				 * @throws IOException
651 				 */
652 				@Override
653 				public void close() throws IOException {
654 					// We don't want to close System.out
655 				}
656 
657 				@Override
658 				public void write(byte[] var0, int var1, int var2) throws IOException {
659 					this.out.write(var0, var1, var2);
660 				}
661 			};
662 		} else if (consoleLog) {
663 			out = new FilterOutputStream(out) {
664 				@Override
665 				public void write(int b) throws IOException {
666 					System.out.write(b);
667 					out.write(b);
668 				}
669 
670 				@Override
671 				public void write(byte[] b) throws IOException {
672 					System.out.write(b);
673 					out.write(b);
674 				}
675 
676 				@Override
677 				public void write(byte[] b, int off, int len) throws IOException {
678 					System.out.write(b, off, len);
679 					out.write(b, off, len);
680 				}
681 			};
682 		}
683 		return logForStream(out);
684 	}
685 
686 	/**
687 	 * Close the trace writer
688 	 *
689 	 * @param traceWriter The trace writer
690 	 */
closeWriter(Writer traceWriter)691 	private void closeWriter(Writer traceWriter) {
692 
693 		if (traceWriter != null) {
694 			try {
695 				traceWriter.close();
696 			} catch (IOException ioEx) {
697 				// we cannot log here; just print the stacktrace.
698 				ioEx.printStackTrace();
699 			}
700 			traceWriter = null;
701 		}
702 	}
703 }