1- import { window as Window , OutputChannel , Progress } from 'vscode' ;
1+ import { window as Window , OutputChannel , Progress , ExtensionContext , Disposable } from 'vscode' ;
22import { DisposableObject } from 'semmle-vscode-utils' ;
3+ import * as fs from 'fs-extra' ;
4+ import * as path from 'path' ;
5+
6+ interface LogOptions {
7+ /** If false, don't output a trailing newline for the log entry. Default true. */
8+ trailingNewline ?: boolean ;
9+
10+ /** If specified, add this log entry to the log file at the specified location. */
11+ additionalLogLocation ?: string ;
12+ }
313
414export interface Logger {
5- /** Writes the given log message, followed by a newline. */
6- log ( message : string ) : void ;
7- /** Writes the given log message, not followed by a newline. */
8- logWithoutTrailingNewline ( message : string ) : void ;
15+ /** Writes the given log message, optionally followed by a newline. */
16+ log ( message : string , options ?: LogOptions ) : Promise < void > ;
917 /**
1018 * Reveal this channel in the UI.
1119 *
1220 * @param preserveFocus When `true` the channel will not take focus.
1321 */
1422 show ( preserveFocus ?: boolean ) : void ;
23+
24+ /**
25+ * Remove the log at the specified location
26+ * @param location log to remove
27+ */
28+ removeAdditionalLogLocation ( location : string ) : Promise < void > ;
1529}
1630
1731export type ProgressReporter = Progress < { message : string } > ;
1832
1933/** A logger that writes messages to an output channel in the Output tab. */
2034export class OutputChannelLogger extends DisposableObject implements Logger {
2135 public readonly outputChannel : OutputChannel ;
36+ private readonly additionalLocations = new Map < string , AdditionalLogLocation > ( ) ;
37+ private additionalLogLocationPath : string | undefined ;
2238
23- constructor ( title : string ) {
39+ constructor ( private title : string ) {
2440 super ( ) ;
2541 this . outputChannel = Window . createOutputChannel ( title ) ;
2642 this . push ( this . outputChannel ) ;
2743 }
2844
29- log ( message : string ) {
30- this . outputChannel . appendLine ( message ) ;
45+ init ( ctx : ExtensionContext ) : void {
46+ this . additionalLogLocationPath = path . join ( ctx . storagePath || ctx . globalStoragePath , this . title ) ;
47+
48+ // clear out any old state from previous runs
49+ fs . remove ( this . additionalLogLocationPath ) ;
3150 }
3251
33- logWithoutTrailingNewline ( message : string ) {
34- this . outputChannel . append ( message ) ;
52+ /**
53+ * This function is asynchronous and will only resolve once the message is written
54+ * to the side log (if required). It is not necessary to await the results of this
55+ * function if you don't need to guarantee that the log writing is complete before
56+ * continuing.
57+ */
58+ async log ( message : string , options = { } as LogOptions ) : Promise < void > {
59+ if ( options . trailingNewline === undefined ) {
60+ options . trailingNewline = true ;
61+ }
62+
63+ if ( options . trailingNewline ) {
64+ this . outputChannel . appendLine ( message ) ;
65+ } else {
66+ this . outputChannel . append ( message ) ;
67+ }
68+
69+ if ( this . additionalLogLocationPath && options . additionalLogLocation ) {
70+ const logPath = path . join ( this . additionalLogLocationPath , options . additionalLogLocation ) ;
71+ let additional = this . additionalLocations . get ( logPath ) ;
72+ if ( ! additional ) {
73+ const msg = `| Log being saved to ${ logPath } |` ;
74+ const separator = new Array ( msg . length ) . fill ( '-' ) . join ( '' ) ;
75+ this . outputChannel . appendLine ( separator ) ;
76+ this . outputChannel . appendLine ( msg ) ;
77+ this . outputChannel . appendLine ( separator ) ;
78+ additional = new AdditionalLogLocation ( logPath ) ;
79+ this . additionalLocations . set ( logPath , additional ) ;
80+ this . track ( additional ) ;
81+ }
82+
83+ await additional . log ( message , options ) ;
84+ }
3585 }
3686
37- show ( preserveFocus ?: boolean ) {
87+ show ( preserveFocus ?: boolean ) : void {
3888 this . outputChannel . show ( preserveFocus ) ;
3989 }
90+
91+ async removeAdditionalLogLocation ( location : string ) : Promise < void > {
92+ if ( this . additionalLogLocationPath ) {
93+ const logPath = path . join ( this . additionalLogLocationPath , location ) ;
94+ const additional = this . additionalLocations . get ( logPath ) ;
95+ if ( additional ) {
96+ this . disposeAndStopTracking ( additional ) ;
97+ this . additionalLocations . delete ( logPath ) ;
98+ }
99+ }
100+ }
101+ }
102+
103+ class AdditionalLogLocation extends Disposable {
104+ constructor ( private location : string ) {
105+ super ( ( ) => { /**/ } ) ;
106+ }
107+
108+ async log ( message : string , options = { } as LogOptions ) : Promise < void > {
109+ if ( options . trailingNewline === undefined ) {
110+ options . trailingNewline = true ;
111+ }
112+ await fs . ensureFile ( this . location ) ;
113+
114+ await fs . appendFile ( this . location , message + ( options . trailingNewline ? '\n' : '' ) , {
115+ encoding : 'utf8'
116+ } ) ;
117+ }
118+
119+ async dispose ( ) : Promise < void > {
120+ await fs . remove ( this . location ) ;
121+ }
40122}
41123
42124/** The global logger for the extension. */
@@ -46,7 +128,9 @@ export const logger = new OutputChannelLogger('CodeQL Extension Log');
46128export const queryServerLogger = new OutputChannelLogger ( 'CodeQL Query Server' ) ;
47129
48130/** The logger for messages from the language server. */
49- export const ideServerLogger = new OutputChannelLogger ( 'CodeQL Language Server' ) ;
131+ export const ideServerLogger = new OutputChannelLogger (
132+ 'CodeQL Language Server'
133+ ) ;
50134
51135/** The logger for messages from tests. */
52136export const testLogger = new OutputChannelLogger ( 'CodeQL Tests' ) ;
0 commit comments