├── .gitignore ├── App.config ├── Extensions ├── EnumerableExtensions.cs └── StringExtensions.cs ├── GlobalSuppressions.cs ├── LICENSE.txt ├── Program.cs ├── README.md ├── RunHiddenConsole.csproj ├── RunHiddenConsole.sln ├── SimpleLog.cs ├── azure-pipelines.yml └── packages.config /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | *.user 3 | bin/ 4 | obj/ 5 | packages/ 6 | _ReSharper\.Caches/ 7 | -------------------------------------------------------------------------------- /App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace PowerShellWindowHost.Extensions 2 | { 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | public static class EnumerableExtensions 7 | { 8 | public static IEnumerable PrependExt(this IEnumerable enumerable, T entity) 9 | { 10 | yield return entity; 11 | foreach (var listEntity in enumerable) 12 | { 13 | yield return listEntity; 14 | } 15 | } 16 | 17 | public static IEnumerable AppendExt(this IEnumerable enumerable, T entity) 18 | { 19 | foreach (var listEntity in enumerable) 20 | { 21 | yield return listEntity; 22 | } 23 | 24 | yield return entity; 25 | } 26 | 27 | public static IEnumerable NotNullOrWhitespaceExt(this IEnumerable strings) 28 | => strings?.Where(s => !string.IsNullOrWhiteSpace(s)) ?? Enumerable.Empty(); 29 | 30 | public static IEnumerable TrimExt(this IEnumerable strings) 31 | => strings.Select(s => s.Trim()); 32 | 33 | public static IEnumerable NotNullExt(this IEnumerable enumerable) 34 | => enumerable?.Where(s => s != null) ?? Enumerable.Empty(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace PowerShellWindowHost.Extensions 2 | { 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | public static class StringExtensions 7 | { 8 | public static IEnumerable SplitExt(this string target, params char[] separators) 9 | => target?.Split(separators) 10 | ?? Enumerable.Empty(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1652:Enable XML documentation output", Justification = "XML Documentation output is not desired.")] 7 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File must have header", Justification = "None.")] 8 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1028:Code must not contain trailing whitespace", Justification = "Just a matter of the autoformatter removing them. Does not disturb readability.")] 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | namespace PowerShellWindowHost 2 | { 3 | using System; 4 | using System.Configuration; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Text.RegularExpressions; 10 | using PowerShellWindowHost.Extensions; 11 | 12 | public static class Program 13 | { 14 | private const string DoubleQuote = "\""; 15 | 16 | public static int Main() 17 | { 18 | ////Debugger.Launch(); 19 | 20 | var arguments = GetArguments(); 21 | 22 | GetAssemblyData(out var executingAssemblyLocation, out var executingAssemblyFileName); 23 | 24 | Configure(executingAssemblyFileName); 25 | 26 | SimpleLog.Info($"Full Commandline: {Environment.CommandLine}"); 27 | SimpleLog.Info($"Detected Attributes: {arguments}"); 28 | 29 | SimpleLog.Info($"RunHiddenConsole Executing Assembly FullName: {executingAssemblyFileName}"); 30 | 31 | var targetExecutablePath = GetTargetExecutablePath(executingAssemblyLocation, executingAssemblyFileName); 32 | if (targetExecutablePath == null) 33 | { 34 | SimpleLog.Error("Unable to find target executable name in own executable name."); 35 | return -7001; 36 | } 37 | 38 | var startInfo = new ProcessStartInfo 39 | { 40 | CreateNoWindow = true, 41 | UseShellExecute = false, 42 | RedirectStandardInput = true, 43 | WindowStyle = ProcessWindowStyle.Hidden, 44 | WorkingDirectory = Directory.GetCurrentDirectory(), 45 | Arguments = arguments, 46 | FileName = targetExecutablePath, 47 | }; 48 | 49 | try 50 | { 51 | var proc = Process.Start(startInfo); 52 | if (proc == null) 53 | { 54 | SimpleLog.Error("Unable to start the target process."); 55 | return -7002; 56 | } 57 | 58 | // process will close as soon as its waiting for interactive input. 59 | proc.StandardInput.Close(); 60 | 61 | proc.WaitForExit(); 62 | 63 | return proc.ExitCode; 64 | } 65 | catch (Exception ex) 66 | { 67 | SimpleLog.Error("Starting target process threw an unknown Exception: " + ex); 68 | SimpleLog.Log(ex); 69 | return -7003; 70 | } 71 | } 72 | 73 | private static void Configure(string executingAssemblyFileName) 74 | { 75 | var logLevelString = ConfigurationManager.AppSettings["LogLevel"]; 76 | 77 | var logLocation = ConfigurationManager.AppSettings["LogLocation"]; 78 | if (logLocation != null) 79 | { 80 | SimpleLog.SetLogDir(logLocation, true); 81 | } 82 | 83 | if (logLevelString != null 84 | && Enum.TryParse(logLevelString, true, out SimpleLog.Severity logLevel)) 85 | { 86 | SimpleLog.LogLevel = logLevel; 87 | } 88 | else 89 | { 90 | SimpleLog.LogLevel = SimpleLog.Severity.Error; 91 | } 92 | 93 | SimpleLog.BackgroundTaskDisabled = true; 94 | SimpleLog.Prefix = $"{executingAssemblyFileName}."; 95 | } 96 | 97 | private static void GetAssemblyData(out string assemblyLocation, out string assemblyFileName) 98 | { 99 | var executingAssembly = Assembly.GetExecutingAssembly(); 100 | assemblyFileName = Path.GetFileName(executingAssembly.Location); 101 | assemblyLocation = Path.GetDirectoryName(executingAssembly.Location) ?? string.Empty; 102 | } 103 | 104 | private static string GetTargetExecutablePath(string executingAssemblyLocation, string executingAssemblyFileName) 105 | { 106 | var match = Regex.Match(executingAssemblyFileName, @"(.+)w(\.\w{1,3})"); 107 | if (!match.Success) 108 | { 109 | return null; 110 | } 111 | 112 | var targetExecutableName = match.Groups[1].Value + match.Groups[2].Value; 113 | 114 | var envPaths = Environment.GetEnvironmentVariable("PATH") 115 | .SplitExt(Path.PathSeparator); 116 | 117 | var configPaths = ConfigurationManager.AppSettings["TargetExecutablePaths"] 118 | .SplitExt(Path.PathSeparator); 119 | 120 | // TODO: this can be somewhat expensive in terms of file io. probably a good idea to cache the result for a few seconds. 121 | var targetExecutablePath = configPaths 122 | .AppendExt(executingAssemblyLocation) 123 | .Concat(envPaths) 124 | .NotNullOrWhitespaceExt() 125 | .TrimExt() 126 | .Select(p => Path.Combine(p, targetExecutableName)) 127 | .FirstOrDefault(File.Exists); 128 | 129 | return targetExecutablePath; 130 | } 131 | 132 | private static string GetArguments() 133 | { 134 | var commandLineExecutable = Environment 135 | .GetCommandLineArgs()[0] 136 | .Trim(); 137 | 138 | var commandLine = Environment 139 | .CommandLine 140 | .Trim(); 141 | 142 | var argsStartIndex = commandLineExecutable.Length 143 | + (commandLine.StartsWith(DoubleQuote) 144 | ? 2 145 | : 0); 146 | 147 | var args = commandLine 148 | .Substring(argsStartIndex) 149 | .Trim(); 150 | 151 | return args; 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RunHiddenConsole 2 | Created executable can be renamed to powershellw.exe or pwshw.exe (or any other console-flashing-window) 3 | and put next to the corresponding assembly. Calls to the added assembly will be sent to a new instance of 4 | the target assembly, which is explicitly started without creating a window. Thus these flashing windows 5 | should be avoided. 6 | 7 | You can find all the magic in Programm.cs 8 | ## Logging 9 | I have added a (cleaned up) copy of simple log (https://www.codeproject.com/Tips/585796/Simple-Log) for 10 | debugging puropse. When there is a crash in the tool you should find a log-file next to your *w.exe called 11 | *w.exe..log which contains some hopefully usefull messages. 12 | 13 | If you need to change the log level of the tool, you can add a "*w.exe.config", and with the default values 14 | as seen in app.config in the repository. Then just update the LogLevel app-setting as you desire. 15 | ## Future Points 16 | - Input stream from file and output stream to file, if requested. 17 | Please tell me how you would like to use this. It would probably require a config file alongside the assembly to configure which parameters define the inputs and outputs 18 | - Windows Eventlog logging. 19 | Might be required by som orgs. But no complaints so far. 20 | 21 | # Keywords 22 | silent, script, hidden, console, window, command, commandline, hide, pwsh, cmd, powershell, bash, pwshw, cmdw, powershellw, exe 23 | -------------------------------------------------------------------------------- /RunHiddenConsole.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {B9ED483F-0DB0-4CCB-B3F6-BE4F03E53B61} 8 | WinExe 9 | PowerShellWindowHost 10 | hiddenw 11 | v4.0 12 | 512 13 | true 14 | true 15 | Client 16 | 17 | 18 | PowerShellWindowHost.Program 19 | 20 | 21 | AnyCPU 22 | true 23 | full 24 | false 25 | bin\Debug\ 26 | DEBUG;TRACE 27 | prompt 28 | 4 29 | 30 | 31 | AnyCPU 32 | pdbonly 33 | true 34 | bin\Release\ 35 | TRACE 36 | prompt 37 | 4 38 | 39 | 40 | PowerShellWindowHost.Program 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /RunHiddenConsole.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28306.52 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RunHiddenConsole", "RunHiddenConsole.csproj", "{B9ED483F-0DB0-4CCB-B3F6-BE4F03E53B61}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {B9ED483F-0DB0-4CCB-B3F6-BE4F03E53B61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {B9ED483F-0DB0-4CCB-B3F6-BE4F03E53B61}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {B9ED483F-0DB0-4CCB-B3F6-BE4F03E53B61}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {B9ED483F-0DB0-4CCB-B3F6-BE4F03E53B61}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {3BA5B66E-C910-42AD-A239-4DD576A9A3C4} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /SimpleLog.cs: -------------------------------------------------------------------------------- 1 | namespace PowerShellWindowHost 2 | { 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Data.SqlClient; 7 | using System.Diagnostics; 8 | using System.IO; 9 | using System.Reflection; 10 | using System.Runtime.InteropServices; 11 | using System.Security.Principal; 12 | using System.Text; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | using System.Xml.Linq; 16 | 17 | /// 18 | /// Simple logging class 19 | /// 20 | /// 21 | /// The MIT License (MIT) 22 | /// 23 | /// Copyright (c) 2013 - 2014 Jochen Scharr 24 | /// 25 | /// Permission is hereby granted, free of charge, to any person obtaining a copy 26 | /// of this software and associated documentation files (the "Software"), to deal 27 | /// in the Software without restriction, including without limitation the rights 28 | /// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 29 | /// copies of the Software, and to permit persons to whom the Software is 30 | /// furnished to do so, subject to the following conditions: 31 | /// 32 | /// The above copyright notice and this permission notice shall be included in 33 | /// all copies or substantial portions of the Software. 34 | /// 35 | /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 36 | /// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 37 | /// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 38 | /// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 39 | /// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 40 | /// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 41 | /// THE SOFTWARE. 42 | /// 43 | /// 44 | /// Just write to a file using static methods. By default, the file is named e.g. "2013_04_21.log" and is written to the current working directory. 45 | /// This class does not depend on anything. It can be used 'out of the box', no configuration necessary. 46 | /// 47 | /// To keep it simple, it contains just the most essential features like 48 | /// 49 | /// - Possibility to customize log directory and file name, 50 | /// - Possibility to pass log entry severity (Info, Warning, Error, Exception), 51 | /// - Convenience methods , , to log with respective severity 52 | /// - Log exceptions, recursively with all inner exceptions, data, stack trace and specific properties of some exception types, 53 | /// - Basic, very simple log level filtering, . 54 | /// - Automatically log source, i.e. class and method name where log method was called from (IMHO a very useful feature), 55 | /// - Log is formatted as an XML fragment, one XML tag per entry. There's a method to complete the XML and get the file as an XML document. 56 | /// - There is a method to display a log file in the browser. Method shows the current log file. 57 | /// - To improve speed, instead of writing each entry to the file immediately, by default, it is passed to a queue and being dequeued and written to disk in a background task. 58 | /// Unfortunately, this makes the class a bit more complicated. 59 | /// - Thread safety. This class can be used from different threads that all write to the same file. The task ID is logged in each item. 60 | /// This way, one can separate afterwards, which thread logged what. 61 | /// - New: Possibility to log directly to disk, without using the background task (thread). See argument "useBackgroundTask" in e.g. or : 62 | /// - New: Possibility to start logging (i.e. the background task) explicitly as opposed to be started automatically on first log. See for details. 63 | /// 64 | /// 65 | /// 66 | /// static void Main(string[] args) 67 | /// { 68 | /// // Log to a sub-directory 'Log' of the current working directory. Prefix log file with 'MyLog_'. 69 | /// // This is an optional call and has only to be done once, pereferably before the first log entry is written. 70 | /// SimpleLog.SetLogFile(".\\Log", "MyLog_"); 71 | /// 72 | /// // Write info message to log 73 | /// SimpleLog.Info("Test logging started."); 74 | /// 75 | /// // Write warning message to log 76 | /// SimpleLog.Warning("This is a warning."); 77 | /// 78 | /// // Write error message to log 79 | /// SimpleLog.Error("This is an error."); 80 | /// 81 | /// try 82 | /// { 83 | /// // For demonstration, do logging in sub-method, throw an exception, 84 | /// // catch it, wrap it in another exception and throw it. 85 | /// DoSomething(); 86 | /// } 87 | /// catch(Exception ex) 88 | /// { 89 | /// // Write exception with all inner exceptions to log 90 | /// SimpleLog.Log(ex); 91 | /// } 92 | /// 93 | /// // Show log file in browser 94 | /// SimpleLog.ShowLogFile(); 95 | /// } 96 | /// 97 | /// private static void DoSomething() 98 | /// { 99 | /// SimpleLog.Info("Entering method. See Source which method is meant."); 100 | /// 101 | /// try 102 | /// { 103 | /// DoSomethingElse(null); 104 | /// } 105 | /// catch(Exception ex) 106 | /// { 107 | /// SimpleLog.Log(ex); 108 | /// throw new InvalidOperationException("Something went wrong.", ex); 109 | /// } 110 | /// } 111 | /// 112 | /// private static void DoSomethingElse(string fred) 113 | /// { 114 | /// SimpleLog.Info("Entering method. See Source which method is meant."); 115 | /// 116 | /// try 117 | /// { 118 | /// // Purposely provoking an exception. 119 | /// int a = fred.IndexOf("Hello"); 120 | /// } 121 | /// catch(Exception ex) 122 | /// { 123 | /// throw new Exception("Something went wrong.", ex); 124 | /// } 125 | /// } 126 | /// 127 | /// 128 | public static class SimpleLog 129 | { 130 | /// 131 | /// Log entry queue 132 | /// 133 | private static readonly Queue LogEntryQueue = new Queue(); 134 | 135 | /// 136 | /// Snyc root for the background task itself 137 | /// 138 | private static readonly object BackgroundTaskSyncRoot = new object(); 139 | 140 | /// 141 | /// Snyc root for the log file 142 | /// 143 | private static readonly object LogFileSyncRoot = new object(); 144 | 145 | /// 146 | /// Directory to log to 147 | /// 148 | /// 149 | /// Default is the application's current working directory 150 | /// 151 | private static DirectoryInfo logDir = new DirectoryInfo(Directory.GetCurrentDirectory()); 152 | 153 | /// 154 | /// Prefix to use in file name 155 | /// 156 | /// 157 | /// Default is the empty string, i.e. no prefix. 158 | /// 159 | private static string prefix; 160 | 161 | /// 162 | /// Date format to use in file name 163 | /// 164 | /// 165 | /// Default is "yyyy_MM_dd" (e.g. 2013_04_21), which leads to a daily change of the log file. 166 | /// 167 | private static string dateFormat; 168 | 169 | /// 170 | /// Suffix to use in file name 171 | /// 172 | /// 173 | /// Default is the empty string, i.e. no suffix. 174 | /// 175 | private static string suffix; 176 | 177 | /// 178 | /// Extension to use in file name 179 | /// 180 | /// 181 | /// Default is "log". 182 | /// 183 | private static string extension; 184 | 185 | /// 186 | /// Background task to write log entries to disk 187 | /// 188 | private static Task backgroundTask; 189 | 190 | /// 191 | /// Backing field for . 192 | /// 193 | private static string textSeparator = " | "; 194 | 195 | /// 196 | /// Initializes static members of the class. 197 | /// 198 | static SimpleLog() 199 | { 200 | // Attach to process exit event 201 | AppDomain.CurrentDomain.ProcessExit += CurrentDomainProcessExit; 202 | } 203 | 204 | /// 205 | /// Log severity 206 | /// 207 | public enum Severity 208 | { 209 | Info, 210 | Warning, 211 | Error, 212 | Exception 213 | } 214 | 215 | /// 216 | /// Gets Directory to log to 217 | /// 218 | /// 219 | /// Default is the application's current working directory. Can be set using . 220 | /// Log file is assembled in using string.Format("{0}\\{1}{2}{3}.{4}", LogDir, Prefix, dateTime.ToString(DateFormat), Suffix, Extension). 221 | /// 222 | public static string LogDir => logDir.FullName; 223 | 224 | /// 225 | /// Gets or sets the Prefix to use in file name 226 | /// 227 | /// 228 | /// Default is the empty string, i.e. no prefix. 229 | /// Log file is assembled in using string.Format("{0}\\{1}{2}{3}.{4}", LogDir, Prefix, dateTime.ToString(DateFormat), Suffix, Extension). 230 | /// 231 | public static string Prefix 232 | { 233 | get => prefix ?? string.Empty; 234 | set => prefix = value; 235 | } 236 | 237 | /// 238 | /// Gets or sets the Suffix to use in file name 239 | /// 240 | /// 241 | /// Default is the empty string, i.e. no suffix. 242 | /// Log file is assembled in using string.Format("{0}\\{1}{2}{3}.{4}", LogDir, Prefix, dateTime.ToString(DateFormat), Suffix, Extension). 243 | /// 244 | public static string Suffix 245 | { 246 | get => suffix ?? string.Empty; 247 | set => suffix = value; 248 | } 249 | 250 | /// 251 | /// Gets or sets the Extension to use in file name 252 | /// 253 | /// 254 | /// Default is "log". Set to null to return to default. 255 | /// Log file is assembled in using string.Format("{0}\\{1}{2}{3}.{4}", LogDir, Prefix, dateTime.ToString(DateFormat), Suffix, Extension). 256 | /// 257 | public static string Extension 258 | { 259 | get => extension ?? "log"; 260 | set => extension = value; 261 | } 262 | 263 | /// 264 | /// Gets or sets the Date format to use in file name 265 | /// 266 | /// 267 | /// Default is "yyyy_MM_dd" (e.g. 2013_04_21), which leads to a daily change of the log file. Set to null to return to default. Set to e.g. "yyyy_MM_dd_HH" to change log file hourly. 268 | /// Log file is assembled in using string.Format("{0}\\{1}{2}{3}.{4}", LogDir, Prefix, dateTime.ToString(DateFormat), Suffix, Extension). 269 | /// 270 | public static string DateFormat 271 | { 272 | get => dateFormat ?? "yyyy_MM_dd"; 273 | set => dateFormat = value; 274 | } 275 | 276 | /// 277 | /// Gets or sets the Log level 278 | /// 279 | /// 280 | /// Log all entries with set here and above. In other words, do not write entries to the log file with 281 | /// severity below the severity specified here. 282 | /// 283 | /// For example, when log level is set to , incoming entries with severity 284 | /// , , and 285 | /// are actually written to the log file. When log level is set to e.g. , only 286 | /// entries with severity and are actually written to the log file. 287 | /// Default is . for details. 288 | /// 289 | public static Severity LogLevel { get; set; } = Severity.Info; 290 | 291 | /// 292 | /// Gets or sets a value indicating whether logging has to be started explicitly as opposed to start automatically on first log. Default is false. 293 | /// 294 | /// 295 | /// Normally, logging starts automatically when the first log entry is enqueued, . In some 296 | /// situations, it may be desired to start logging explicitly at a later time. In the meantime, logging 297 | /// entries are enqued and are processed (i.e. written to the log file) when logging is started. 298 | /// To start logging, use 299 | /// 300 | public static bool StartExplicitly 301 | { 302 | get; 303 | set; 304 | } 305 | 306 | /// 307 | /// Gets or sets a value indicating whether to write plain text instead of XML. Default is false. 308 | /// 309 | public static bool WriteText 310 | { 311 | get; 312 | set; 313 | } 314 | 315 | /// 316 | /// Gets or sets the separator text entries reperesenting attributes or values are separated with, when is true. Defaults to " | ". 317 | /// 318 | public static string TextSeparator 319 | { 320 | get => textSeparator; 321 | set => textSeparator = value ?? string.Empty; 322 | } 323 | 324 | /// 325 | /// Gets file to log into 326 | /// 327 | /// 328 | /// Is assembled from , , the current date and time formatted in , 329 | /// , "." and . So, by default, the file is named e.g. "2013_04_21.log" and is written to the current working directory. 330 | /// It is assembled in using string.Format("{0}\\{1}{2}{3}.{4}", LogDir, Prefix, dateTime.ToString(DateFormat), Suffix, Extension). 331 | /// 332 | public static string FileName => GetFileName(DateTime.Now); 333 | 334 | /// 335 | /// Gets a value indicating whether to stop enqueing new entries. 336 | /// 337 | /// 338 | /// Use to stop logging and to start logging. 339 | /// 340 | public static bool StopEnqueingNewEntries 341 | { 342 | get; 343 | private set; 344 | } 345 | 346 | /// 347 | /// Gets a value indicating whether to stop logging background task is requested, i.e. to stop logging at all is requested. 348 | /// 349 | /// 350 | /// Use to stop logging and to start logging. 351 | /// 352 | public static bool StopLoggingRequested 353 | { 354 | get; 355 | private set; 356 | } 357 | 358 | /// 359 | /// Gets the last exception that occurred in the background task when trying to write to the file. 360 | /// 361 | public static Exception LastExceptionInBackgroundTask 362 | { 363 | get; 364 | private set; 365 | } 366 | 367 | /// 368 | /// Gets the number of log entries waiting to be written to file 369 | /// 370 | /// 371 | /// When this number is 1000 or more, there seems to be a permanent problem to wite 372 | /// to the file. See what it could be. 373 | /// 374 | public static int NumberOfLogEntriesWaitingToBeWrittenToFile => LogEntryQueue.Count; 375 | 376 | /// 377 | /// Gets a value indicating whether logging background task currenty runs, i.e. 378 | /// log entries are written to disk. 379 | /// 380 | /// 381 | /// If logging is not running (yet), log methods can be called anyway. Messages will 382 | /// be written to disk when logging is started. See for details. 383 | /// 384 | public static bool LoggingStarted => backgroundTask != null; 385 | 386 | /// 387 | /// Gets or sets a value indicating whether background task logging is disabled, 388 | /// and thus immediate logging is executed. 389 | /// 390 | public static bool BackgroundTaskDisabled { get; set; } = false; 391 | 392 | /// 393 | /// Set all log properties at once 394 | /// 395 | /// 396 | /// Set all log customizing properties at once. This is a pure convenience function. All parameters are optional. 397 | /// When is set and it cannot be created or writing a first entry fails, no exception is thrown, but the previous directory, 398 | /// respectively the default directory (the current working directory), is used instead. 399 | /// 400 | /// for details. When null is passed here, is not set. Here, is created, when it does not exist. 401 | /// for details. When null is passed here, is not set. 402 | /// for details. When null is passed here, is not set. 403 | /// for details. When null is passed here, is not set. 404 | /// for details. When null is passed here, is not set. 405 | /// for details. When null is passed here, is not set. 406 | /// for details. When null is passed here, is not set. 407 | /// Whether to call , i.e. whether to write a test entry after setting the new log file. If true, the result of is returned. 408 | /// for details. When null is passed here, is not set. 409 | /// for details. When null is passed here, is not set. 410 | /// Null on success, otherwise an exception with what went wrong. 411 | public static Exception SetLogFile(string updatedLogDirectory = null, string updatedPrefix = null, string updatedSuffix = null, string updatedExtension = null, string updatedDateFormat = null, Severity? logLevel = null, bool? startExplicitly = null, bool check = true, bool? writeText = null, string updatedTextSeparator = null) 412 | { 413 | Exception result = null; 414 | 415 | try 416 | { 417 | if (writeText != null) 418 | { 419 | WriteText = writeText.Value; 420 | } 421 | 422 | if (updatedTextSeparator != null) 423 | { 424 | TextSeparator = updatedTextSeparator; 425 | } 426 | 427 | if (logLevel != null) 428 | { 429 | LogLevel = logLevel.Value; 430 | } 431 | 432 | if (updatedExtension != null) 433 | { 434 | Extension = updatedExtension; 435 | } 436 | 437 | if (updatedSuffix != null) 438 | { 439 | Suffix = updatedSuffix; 440 | } 441 | 442 | if (updatedDateFormat != null) 443 | { 444 | DateFormat = updatedDateFormat; 445 | } 446 | 447 | if (updatedPrefix != null) 448 | { 449 | Prefix = updatedPrefix; 450 | } 451 | 452 | if (startExplicitly != null) 453 | { 454 | StartExplicitly = startExplicitly.Value; 455 | } 456 | 457 | if (updatedLogDirectory != null) 458 | { 459 | result = SetLogDir(updatedLogDirectory, true); 460 | } 461 | 462 | // Check if logging works with new settings 463 | if (result == null && check) 464 | { 465 | result = Check(); 466 | } 467 | } 468 | catch (Exception ex) 469 | { 470 | result = ex; 471 | } 472 | 473 | return result; 474 | } 475 | 476 | /// 477 | /// Set new logging directory 478 | /// 479 | /// The logging diretory to set. When passing null or the empty string, the current working directory is used. 480 | /// Try to create directory if not existing. Default is false. 481 | /// Null if setting log directory was successful, otherwise an exception with what went wrong. 482 | public static Exception SetLogDir(string logDirectory, bool createIfNotExisting = false) 483 | { 484 | if (string.IsNullOrEmpty(logDirectory)) 485 | { 486 | logDirectory = Directory.GetCurrentDirectory(); 487 | } 488 | 489 | try 490 | { 491 | logDir = new DirectoryInfo(logDirectory); 492 | 493 | if (!logDir.Exists) 494 | { 495 | if (createIfNotExisting) 496 | { 497 | logDir.Create(); 498 | } 499 | else 500 | { 501 | throw new DirectoryNotFoundException($"Directory '{logDir.FullName}' does not exist!"); 502 | } 503 | } 504 | } 505 | catch (Exception ex) 506 | { 507 | return ex; 508 | } 509 | 510 | return null; 511 | } 512 | 513 | /// 514 | /// Check if logging to works 515 | /// 516 | /// 517 | /// Writes a test entry directly to without using the background task. 518 | /// When no exception is returned, logging to works. 519 | /// 520 | /// Test message to write to the log file 521 | /// Null on success, otherwise an exception with what went wrong. 522 | public static Exception Check(string message = "Test entry to see if logging works.") 523 | { 524 | // Try to write directly to the file to see if it's working. 525 | return Log(message, Severity.Info, false); 526 | } 527 | 528 | /// 529 | /// Write info message to log 530 | /// 531 | /// The message to write to the log 532 | /// Whether to use the background task (thread) to write messages to disk. Default is true. This is much faster than writing directly to disk in the main thread. 533 | /// Null on success or the that occurred when processing the message, i.e. when enqueuing the message (when is true) or when writing the message to disk (when is false). 534 | public static Exception Info(string message, bool useBackgroundTask = true) 535 | { 536 | return Log(message, Severity.Info, useBackgroundTask); 537 | } 538 | 539 | /// 540 | /// Write warning message to log 541 | /// 542 | /// The message to write to the log 543 | /// Whether to use the background task (thread) to write messages to disk. Default is true. This is much faster than writing directly to disk in the main thread. 544 | /// Null on success or the that occurred when processing the message, i.e. when enqueuing the message (when is true) or when writing the message to disk (when is false). 545 | public static Exception Warning(string message, bool useBackgroundTask = true) 546 | { 547 | return Log(message, Severity.Warning, useBackgroundTask); 548 | } 549 | 550 | /// 551 | /// Write error message to log 552 | /// 553 | /// The message to write to the log 554 | /// Whether to use the background task (thread) to write messages to disk. Default is true. This is much faster than writing directly to disk in the main thread. 555 | /// Null on success or the that occurred when processing the message, i.e. when enqueuing the message (when is true) or when writing the message to disk (when is false). 556 | public static Exception Error(string message, bool useBackgroundTask = true) 557 | { 558 | return Log(message, Severity.Error, useBackgroundTask); 559 | } 560 | 561 | /// 562 | /// Write exception to log 563 | /// 564 | /// The exception to write to the log 565 | /// Whether to use the background task (thread) to write messages to disk. Default is true. This is much faster than writing directly to disk in the main thread. 566 | /// How many frames to skip when detecting the calling method, . This is useful when log calls to are wrapped in an application. Default is 0. 567 | /// Null on success or the that occurred when processing the message, i.e. when enqueuing the message (when is true) or when writing the message to disk (when is false). 568 | public static Exception Log(Exception ex, bool useBackgroundTask = true, int framesToSkip = 0) 569 | { 570 | return ex == null ? null : Log(GetExceptionXElement(ex), Severity.Exception, useBackgroundTask, framesToSkip); 571 | } 572 | 573 | /// 574 | /// Gets an XML string with detailed information about an exception 575 | /// 576 | /// 577 | /// Recursively adds elements for inner exceptions. For the most inner exception, the stack trace is added. 578 | /// Tags for are added. Specific properties of the exception types , 579 | /// and are recognized, too. 580 | /// 581 | /// The exception to get detailed information about 582 | /// An XML string with detailed information about the passed exception 583 | public static string GetExceptionAsXmlString(Exception ex) 584 | { 585 | var xElement = GetExceptionXElement(ex); 586 | return xElement?.ToString() ?? string.Empty; 587 | } 588 | 589 | /// 590 | /// Gets an XElement for an exception 591 | /// 592 | /// 593 | /// Recursively adds elements for inner exceptions. For the most inner exception, the stack trace is added. 594 | /// Tags for are added. Specific properties of the exception types , 595 | /// and are recognized, too. 596 | /// 597 | /// The exception to get the XElement for 598 | /// An XElement for the exception 599 | public static XElement GetExceptionXElement(Exception ex) 600 | { 601 | if (ex == null) 602 | { 603 | return null; 604 | } 605 | 606 | var xElement = new XElement("Exception"); 607 | xElement.Add(new XAttribute("Type", ex.GetType().FullName ?? "unknown")); 608 | xElement.Add(new XAttribute("Source", ex.TargetSite == null || ex.TargetSite.DeclaringType == null ? ex.Source : $"{ex.TargetSite.DeclaringType.FullName}.{ex.TargetSite.Name}")); 609 | xElement.Add(new XElement("Message", ex.Message)); 610 | 611 | if (ex.Data.Count > 0) 612 | { 613 | var xDataElement = new XElement("Data"); 614 | 615 | foreach (DictionaryEntry de in ex.Data) 616 | { 617 | xDataElement.Add(new XElement("Entry", new XAttribute("Key", de.Key), new XAttribute("Value", de.Value ?? string.Empty))); 618 | } 619 | 620 | xElement.Add(xDataElement); 621 | } 622 | 623 | switch (ex) 624 | { 625 | case SqlException sqlEx: 626 | var xSqlElement = new XElement("SqlException"); 627 | xSqlElement.Add(new XAttribute("ErrorNumber", sqlEx.Number)); 628 | 629 | if (!string.IsNullOrEmpty(sqlEx.Server)) 630 | { 631 | xSqlElement.Add(new XAttribute("ServerName", sqlEx.Server)); 632 | } 633 | 634 | if (!string.IsNullOrEmpty(sqlEx.Procedure)) 635 | { 636 | xSqlElement.Add(new XAttribute("Procedure", sqlEx.Procedure)); 637 | } 638 | 639 | xElement.Add(xSqlElement); 640 | break; 641 | case COMException comEx: 642 | var xComElement = new XElement("ComException"); 643 | xComElement.Add(new XAttribute("ErrorCode", $"0x{(uint)comEx.ErrorCode:X8}")); 644 | xElement.Add(xComElement); 645 | break; 646 | case AggregateException exception: 647 | var xAggElement = new XElement("AggregateException"); 648 | foreach (var innerEx in exception.InnerExceptions) 649 | { 650 | xAggElement.Add(GetExceptionXElement(innerEx)); 651 | } 652 | 653 | xElement.Add(xAggElement); 654 | break; 655 | } 656 | 657 | xElement.Add(ex.InnerException == null ? new XElement("StackTrace", ex.StackTrace) : GetExceptionXElement(ex.InnerException)); 658 | 659 | return xElement; 660 | } 661 | 662 | /// 663 | /// Write message to log 664 | /// 665 | /// 666 | /// See . 667 | /// 668 | /// The message to write to the log 669 | /// Log entry severity 670 | /// Whether to use the background task (thread) to write messages to disk. Default is true. This is much faster than writing directly to disk in the main thread. 671 | /// How many frames to skip when detecting the calling method, . This is useful when log calls to are wrapped in an application. Default is 0. 672 | /// Null on success or the that occurred when processing the message, i.e. when enqueuing the message (when is true) or when writing the message to disk (when is false). 673 | public static Exception Log(string message, Severity severity = Severity.Info, bool useBackgroundTask = true, int framesToSkip = 0) 674 | { 675 | return string.IsNullOrEmpty(message) ? null : Log(new XElement("Message", message), severity, useBackgroundTask, framesToSkip); 676 | } 677 | 678 | /// 679 | /// Write XElement to log 680 | /// 681 | /// 682 | /// Unless is set to false (default is true), the XElement is not actually 683 | /// written to the file here, but enqueued to the log entry queue. It is dequeued by 684 | /// in a backround task and actually written to the file there. 685 | /// This is much faster than writing directly to disk in the main thread (what is done when 686 | /// is set to false). 687 | /// 688 | /// However, writing to the file is synchronized between threads. I.e. writing directly can be done from multiple threads. 689 | /// Also, using the background task and writing directly to the file can be used both in parallel. 690 | /// 691 | /// When is set to true (default is false), the background task must be started 692 | /// explicitly by calling , to get messages actually written to the file. They get enqueued 693 | /// before the background task is started, though. I.e. they will get logged when the background task is started later. 694 | /// 695 | /// When is set to false, which is the default, logging background task (thread) is 696 | /// started automatically when first calling this method with set to true 697 | /// (which is the default). 698 | /// 699 | /// The XElement to log 700 | /// Log entry severity, defaults to 701 | /// Whether to use the background task (thread) to write messages to disk. Default is true. This is much faster than writing directly to disk in the main thread. 702 | /// How many frames to skip when detecting the calling method, . This is useful when log calls to are wrapped in an application. Default is 0. 703 | /// Null on success or the that occurred when processing the message, i.e. when enqueuing the message (when is true) or when writing the message to disk (when is false). 704 | public static Exception Log(XElement xElement, Severity severity = Severity.Info, bool useBackgroundTask = true, int framesToSkip = 0) 705 | { 706 | // Filter entries below log level 707 | if (xElement == null || severity < LogLevel) 708 | { 709 | return null; 710 | } 711 | 712 | try 713 | { 714 | // Assemble XML log entry 715 | var logEntry = new XElement("LogEntry"); 716 | logEntry.Add(new XAttribute("Date", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))); 717 | logEntry.Add(new XAttribute("Severity", severity)); 718 | logEntry.Add(new XAttribute("Source", GetCaller(framesToSkip))); 719 | logEntry.Add(new XAttribute("ThreadId", Thread.CurrentThread.ManagedThreadId)); 720 | logEntry.Add(xElement); 721 | 722 | if (useBackgroundTask && !BackgroundTaskDisabled) 723 | { 724 | // Enqueue log entry to be written to the file by background task 725 | Enqueue(logEntry); 726 | } 727 | else 728 | { 729 | // Write directly to the file. This is synchronized among threads within the method, 730 | // so can be used in parallel with the above. 731 | return WriteLogEntryToFile(logEntry); 732 | } 733 | } 734 | catch (Exception ex) 735 | { 736 | return ex; 737 | } 738 | 739 | return null; 740 | } 741 | 742 | /// 743 | /// Gets the log filename for the passed date 744 | /// 745 | /// The date to get the log file name for 746 | /// The log filename for the passed date 747 | public static string GetFileName(DateTime dateTime) 748 | { 749 | return $"{LogDir}\\{Prefix}{dateTime.ToString(DateFormat)}{Suffix}.{Extension}"; 750 | } 751 | 752 | /// 753 | /// Check, whether there is a log file for the passed date 754 | /// 755 | /// The date and time to check the existance of a log file for 756 | /// True = log file exists, false otherwise 757 | public static bool LogFileExists(DateTime dateTime) 758 | { 759 | return File.Exists(GetFileName(dateTime)); 760 | } 761 | 762 | /// 763 | /// Get the current log file as XML document 764 | /// 765 | /// 766 | /// Does not throw an exception when the log file does not exist. 767 | /// 768 | /// The log file as XML document or null when it does not exist. 769 | public static XDocument GetLogFileAsXml() 770 | { 771 | return GetLogFileAsXml(DateTime.Now); 772 | } 773 | 774 | /// 775 | /// Get the log file for the passed date as XML document 776 | /// 777 | /// 778 | /// Does not throw an exception when the log file does not exist. 779 | /// 780 | /// The date and time to get the log file for. Use DateTime.Now to get the current log file. 781 | /// The log file as XML document or null when it does not exist. 782 | public static XDocument GetLogFileAsXml(DateTime dateTime) 783 | { 784 | var fileName = GetFileName(dateTime); 785 | if (!File.Exists(fileName)) 786 | { 787 | return null; 788 | } 789 | 790 | Flush(); 791 | 792 | var sb = new StringBuilder(); 793 | sb.AppendLine(""); 794 | sb.AppendLine(""); 795 | sb.AppendLine(File.ReadAllText(fileName)); 796 | sb.AppendLine(""); 797 | 798 | return XDocument.Parse(sb.ToString()); 799 | } 800 | 801 | /// 802 | /// Get the current log file as text document 803 | /// 804 | /// 805 | /// Does not throw an exception when the log file does not exist. 806 | /// 807 | /// The log file as text document or null when it does not exist. 808 | public static string GetLogFileAsText() 809 | { 810 | return GetLogFileAsText(DateTime.Now); 811 | } 812 | 813 | /// 814 | /// Get the log file for the passed date as text document 815 | /// 816 | /// 817 | /// Does not throw an exception when the log file does not exist. 818 | /// 819 | /// The date and time to get the log file for. Use DateTime.Now to get the current log file. 820 | /// The log file as text document or null when it does not exist. 821 | public static string GetLogFileAsText(DateTime dateTime) 822 | { 823 | var fileName = GetFileName(dateTime); 824 | if (!File.Exists(fileName)) 825 | { 826 | return null; 827 | } 828 | 829 | Flush(); 830 | 831 | return File.ReadAllText(fileName); 832 | } 833 | 834 | /// 835 | /// Shows the current log file 836 | /// 837 | /// 838 | /// Opens the default program to show text or XML files and displays the requested file, if it exists. Does nothing otherwise. 839 | /// When is false, a temporary XML file is created and saved in the users's temporary path each time this method is called. 840 | /// So don't use it excessively in that case. Otherwise, the log file itself is shown. 841 | /// 842 | public static void ShowLogFile() 843 | { 844 | ShowLogFile(DateTime.Now); 845 | } 846 | 847 | /// 848 | /// Show a log file for the passed date 849 | /// 850 | /// 851 | /// Opens the default program to show text or XML files and displays the requested file, if it exists. Does nothing otherwise. 852 | /// When is false, a temporary XML file is created and saved in the users's temporary path each time this method is called. 853 | /// So don't use it excessively in that case. Otherwise, the log file itself is shown. 854 | /// 855 | /// The date and time to show the log file for. 856 | public static void ShowLogFile(DateTime dateTime) 857 | { 858 | string fileName; 859 | 860 | if (WriteText) 861 | { 862 | Flush(); 863 | fileName = GetFileName(dateTime); 864 | } 865 | else 866 | { 867 | fileName = $"{Path.GetTempPath()}Log_{DateTime.Now:yyyyMMddHHmmssffff}.xml"; 868 | var logFileXml = GetLogFileAsXml(dateTime); 869 | logFileXml?.Save(fileName); 870 | } 871 | 872 | if (!File.Exists(fileName)) 873 | { 874 | return; 875 | } 876 | 877 | // Let system choose application to start 878 | Process.Start(fileName); 879 | 880 | // Wait a little to give application time to open 881 | Thread.Sleep(2000); 882 | } 883 | 884 | /// 885 | /// Start logging 886 | /// 887 | /// 888 | /// Start background task pointing to to write log files to disk. 889 | /// Is called automatically by when the first entry is logged, unless 890 | /// is set to true (default is false). Then, this method has to be 891 | /// called explicitly to start logging. 892 | /// 893 | public static void StartLogging() 894 | { 895 | // Task already started 896 | if (backgroundTask != null || StopEnqueingNewEntries || StopLoggingRequested) 897 | { 898 | return; 899 | } 900 | 901 | // Reset stopping flags 902 | StopEnqueingNewEntries = false; 903 | StopLoggingRequested = false; 904 | 905 | lock (BackgroundTaskSyncRoot) 906 | { 907 | if (backgroundTask != null) 908 | { 909 | return; 910 | } 911 | 912 | // Reset last exception 913 | LastExceptionInBackgroundTask = null; 914 | 915 | // Create and start task 916 | backgroundTask = new Task(WriteLogEntriesToFile, TaskCreationOptions.LongRunning); 917 | backgroundTask.Start(); 918 | } 919 | } 920 | 921 | /// 922 | /// Stop logging background task, i.e. logging at all. 923 | /// 924 | /// 925 | /// Stop background task pointing to to write log files to disk. 926 | /// 927 | /// Whether to write all pending entries to disk before. Default is true. 928 | public static void StopLogging(bool flush = true) 929 | { 930 | // Stop enqueing new log entries. 931 | StopEnqueingNewEntries = true; 932 | 933 | // Useless to go on ... 934 | if (backgroundTask == null) 935 | { 936 | return; 937 | } 938 | 939 | // Write pending entries to disk. 940 | if (flush) 941 | { 942 | Flush(); 943 | } 944 | 945 | // Now tell the background task to stop. 946 | StopLoggingRequested = true; 947 | 948 | lock (BackgroundTaskSyncRoot) 949 | { 950 | if (backgroundTask == null) 951 | { 952 | return; 953 | } 954 | 955 | // Wait for task to finish and set null then. 956 | backgroundTask.Wait(1000); 957 | backgroundTask = null; 958 | } 959 | } 960 | 961 | /// 962 | /// Wait for all entries having been written to the file 963 | /// 964 | public static void Flush() 965 | { 966 | // Background task not running? Nothing to do. 967 | if (!LoggingStarted) 968 | { 969 | return; 970 | } 971 | 972 | // Are there still items waiting to be written to disk? 973 | while (NumberOfLogEntriesWaitingToBeWrittenToFile > 0) 974 | { 975 | // Remember current number 976 | var lastNumber = NumberOfLogEntriesWaitingToBeWrittenToFile; 977 | 978 | // Wait some time to let background task do its work 979 | Thread.Sleep(222); 980 | 981 | // Didn't help? No log entries have been processed? We probably hang. 982 | // Let it be to avoid waiting eternally. 983 | if (lastNumber == NumberOfLogEntriesWaitingToBeWrittenToFile) 984 | { 985 | break; 986 | } 987 | } 988 | } 989 | 990 | /// 991 | /// Clear background task's log entry queue. I.e. remove all log messages waiting to be written to by the background task. 992 | /// 993 | public static void ClearQueue() 994 | { 995 | lock (LogEntryQueue) 996 | { 997 | LogEntryQueue.Clear(); 998 | } 999 | } 1000 | 1001 | /// 1002 | /// Process is about to exit 1003 | /// 1004 | /// 1005 | /// This is some kind of static destructor used to flush unwritten log entries. 1006 | /// 1007 | /// Event Sender 1008 | /// Event Args 1009 | private static void CurrentDomainProcessExit(object sender, EventArgs e) 1010 | { 1011 | StopLogging(); 1012 | } 1013 | 1014 | /// 1015 | /// Enqueue log entry to be written to log file 1016 | /// 1017 | /// 1018 | /// When is set to false (which is the default), 1019 | /// logging is started automatically by calling from 1020 | /// inside this method when the first is enqueued. 1021 | /// 1022 | /// When is set to true, 1023 | /// is just enqueued, but not yet actually written to the log file. 1024 | /// The latter will be done when is called explicitly. 1025 | /// 1026 | /// The log entry to be enqueued 1027 | private static void Enqueue(XElement logEntry) 1028 | { 1029 | // Stop enqueuing when instructed to do so 1030 | if (StopEnqueingNewEntries) 1031 | { 1032 | return; 1033 | } 1034 | 1035 | // Start logging if not already started, unless it is desired to start it explicitly 1036 | if (!StartExplicitly) 1037 | { 1038 | StartLogging(); 1039 | } 1040 | 1041 | lock (LogEntryQueue) 1042 | { 1043 | // Stop enqueueing when the queue gets too full. 1044 | if (LogEntryQueue.Count < 10000) 1045 | { 1046 | LogEntryQueue.Enqueue(logEntry); 1047 | } 1048 | } 1049 | } 1050 | 1051 | /// 1052 | /// Get the next log entry from the queue, but do not dequeue it 1053 | /// 1054 | /// The next element or null when the queue is empty 1055 | private static XElement Peek() 1056 | { 1057 | lock (LogEntryQueue) 1058 | { 1059 | return LogEntryQueue.Count == 0 ? null : LogEntryQueue.Peek(); 1060 | } 1061 | } 1062 | 1063 | /// 1064 | /// Dequeue log entry 1065 | /// 1066 | private static void Dequeue() 1067 | { 1068 | lock (LogEntryQueue) 1069 | { 1070 | if (LogEntryQueue.Count > 0) 1071 | { 1072 | LogEntryQueue.Dequeue(); 1073 | } 1074 | } 1075 | } 1076 | 1077 | /// 1078 | /// Write log entries to the file on disk 1079 | /// 1080 | /// 1081 | /// The thread looks every 100 milliseconds for new items in the queue. 1082 | /// 1083 | private static void WriteLogEntriesToFile() 1084 | { 1085 | while (!StopLoggingRequested) 1086 | { 1087 | // Get next log entry from queue 1088 | var xmlEntry = Peek(); 1089 | if (xmlEntry == null) 1090 | { 1091 | // If queue is empty, sleep for a while and look again later. 1092 | Thread.Sleep(100); 1093 | continue; 1094 | } 1095 | 1096 | // Try ten times to write the entry to the log file. Wait between tries, because the file could (hopefully) temporarily 1097 | // be locked by another application. When it didn't work out after ten tries, dequeue the entry anyway, i.e. the entry is lost then. 1098 | // This is necessary to ensure that the queue does not get too full and we run out of memory. 1099 | for (var i = 0; i < 10; i++) 1100 | { 1101 | // Actually write entry to log file. 1102 | var ex = WriteLogEntryToFile(xmlEntry); 1103 | WriteOwnExceptionToEventLog(ex); 1104 | LastExceptionInBackgroundTask = ex; 1105 | 1106 | // When all is fine, we're done. Otherwise do not retry when queue is already getting full. 1107 | if (LastExceptionInBackgroundTask == null || NumberOfLogEntriesWaitingToBeWrittenToFile > 1000) 1108 | { 1109 | break; 1110 | } 1111 | 1112 | // Only wait when queue is not already getting full. 1113 | Thread.Sleep(100); 1114 | } 1115 | 1116 | // Dequeue entry from the queue 1117 | Dequeue(); 1118 | } 1119 | } 1120 | 1121 | /// 1122 | /// Write exceptions happening here, i.e. to the event log. 1123 | /// 1124 | /// 1125 | /// When there are exceptions occurring when we try to write to disk, sometimes it is hard to find out why this fails. 1126 | /// That's why they are written to the event log here. For not to clutter up event log too much, doubles are sorted out. 1127 | /// The event is written as an error to the application event log under source "SimpleLog". 1128 | /// 1129 | /// The exception to write to the event log. 1130 | private static void WriteOwnExceptionToEventLog(Exception ex) 1131 | { 1132 | // Filter out doubles for not to clutter up exception log. 1133 | if (ex == null || (LastExceptionInBackgroundTask != null && ex.Message == LastExceptionInBackgroundTask.Message)) 1134 | { 1135 | return; 1136 | } 1137 | 1138 | try 1139 | { 1140 | const string Source = "SimpleLog"; 1141 | const string LogName = "Application"; 1142 | string message; 1143 | 1144 | try 1145 | { 1146 | var xElement = GetExceptionXElement(ex); 1147 | message = xElement.ToString(); 1148 | } 1149 | catch 1150 | { 1151 | message = ex.Message; 1152 | } 1153 | 1154 | if (!EventLog.SourceExists(Source)) 1155 | { 1156 | EventLog.CreateEventSource(Source, LogName); 1157 | } 1158 | 1159 | EventLog.WriteEntry(Source, message, EventLogEntryType.Error, 0); 1160 | } 1161 | catch 1162 | { 1163 | // ignored 1164 | } 1165 | } 1166 | 1167 | /// 1168 | /// Write one log entry to file 1169 | /// 1170 | /// 1171 | /// This method can be called from the logging background thread or directly 1172 | /// from the main thread. Lock accordingly to avoid multiple threads concurrently 1173 | /// accessing the file. When the lock can not be got within five seconds, 1174 | /// is not being written to the file, but a respective 1175 | /// exception is returned, saying what went wrong. 1176 | /// 1177 | /// The entry to write 1178 | /// Null when all worked fine, an exception otherwise 1179 | private static Exception WriteLogEntryToFile(XElement xmlEntry) 1180 | { 1181 | if (xmlEntry == null) 1182 | { 1183 | return null; 1184 | } 1185 | 1186 | const int SecondsToWaitForFile = 5; 1187 | 1188 | // This method can be called from the logging background thread or directly 1189 | // from the main thread. Lock accordingly to avoid multiple threads concurrently 1190 | // accessing the file. 1191 | if (Monitor.TryEnter(LogFileSyncRoot, new TimeSpan(0, 0, 0, SecondsToWaitForFile))) 1192 | { 1193 | try 1194 | { 1195 | // Use filestream to be able to explicitly specify FileShare.None 1196 | using (var fileStream = new FileStream(FileName, FileMode.Append, FileAccess.Write, FileShare.None)) 1197 | { 1198 | using (var streamWriter = new StreamWriter(fileStream)) 1199 | { 1200 | if (WriteText) 1201 | { 1202 | // Write plain text 1203 | streamWriter.WriteLine(ConvertXmlToPlainText(xmlEntry)); 1204 | } 1205 | else 1206 | { 1207 | // Write XML 1208 | streamWriter.WriteLine(xmlEntry); 1209 | } 1210 | } 1211 | } 1212 | 1213 | return null; 1214 | } 1215 | catch (Exception ex) 1216 | { 1217 | try 1218 | { 1219 | ex.Data["Filename"] = FileName; 1220 | } 1221 | catch 1222 | { 1223 | // ignored 1224 | } 1225 | 1226 | try 1227 | { 1228 | var user = WindowsIdentity.GetCurrent(); 1229 | ex.Data["Username"] = user.Name; 1230 | } 1231 | catch 1232 | { 1233 | // ignored 1234 | } 1235 | 1236 | return ex; 1237 | } 1238 | finally 1239 | { 1240 | Monitor.Exit(LogFileSyncRoot); 1241 | } 1242 | } 1243 | 1244 | try 1245 | { 1246 | return new Exception( 1247 | $"Could not write to file '{FileName}', because it was blocked by another thread for more than {SecondsToWaitForFile} seconds."); 1248 | } 1249 | catch (Exception ex) 1250 | { 1251 | return ex; 1252 | } 1253 | } 1254 | 1255 | /// 1256 | /// Convert to plain text to be written to a file. 1257 | /// 1258 | /// 1259 | /// A typical xml entry to be converted looks like this: 1260 | /// 1263 | /// Entering method. See Source which method is meant. 1264 | /// 1265 | /// 1266 | /// 1267 | /// Something went wrong. 1268 | /// 1269 | /// Object reference not set to an instance of an object. 1270 | /// at SimpleLogDemo.Program.DoSomethingElse(String fred) in D:\Projekt\VisualStudio\SimpleLogDemo\SimpleLogDemo\Program.cs:line 91 1271 | /// 1272 | /// 1273 | /// 1274 | /// 1275 | /// ]]> 1276 | /// 1277 | /// This is a basic implementation so far. Feel free to implement your own if you need something more sophisticated, e.g. 1278 | /// nicer exception formatting. 1279 | /// 1280 | /// The XML entry to convert. 1281 | /// converted to plain text. 1282 | private static string ConvertXmlToPlainText(XElement xmlEntry) 1283 | { 1284 | var sb = new StringBuilder(); 1285 | 1286 | foreach (var element in xmlEntry.DescendantsAndSelf()) 1287 | { 1288 | if (element.HasAttributes) 1289 | { 1290 | foreach (var attribute in element.Attributes()) 1291 | { 1292 | if (sb.Length > 0) 1293 | { 1294 | sb.Append(TextSeparator); 1295 | } 1296 | 1297 | sb.Append(attribute.Name).Append(" = ").Append(attribute.Value); 1298 | } 1299 | } 1300 | else 1301 | { 1302 | if (sb.Length > 0) 1303 | { 1304 | sb.Append(TextSeparator); 1305 | } 1306 | 1307 | // Remove new lines to get all in one line. 1308 | var value = element.Value.Replace("\r\n", " "); 1309 | sb.Append(element.Name).Append(" = ").Append(value); 1310 | } 1311 | } 1312 | 1313 | return sb.ToString(); 1314 | } 1315 | 1316 | /// 1317 | /// Detects the method that was calling the log method 1318 | /// 1319 | /// 1320 | /// The method is walking up the frames in the stack trace until the first method outside is reached. 1321 | /// When log calls to are wrapped in an application, this may still not be the method where logging 1322 | /// was called initially (e.g. when an exception occurred and has been logged). In that case set 1323 | /// accordingly to get outside the wrapper method(s). 1324 | /// 1325 | /// How many frames to skip when detecting the calling method. This is useful when log calls to are wrapped in an application. Default is 0. 1326 | /// Class and method that was calling the log method 1327 | private static string GetCaller(int framesToSkip = 0) 1328 | { 1329 | var result = string.Empty; 1330 | 1331 | var i = 1; 1332 | 1333 | while (true) 1334 | { 1335 | // Walk up the stack trace ... 1336 | var stackFrame = new StackFrame(i++); 1337 | var methodBase = stackFrame.GetMethod(); 1338 | if (methodBase == null) 1339 | { 1340 | break; 1341 | } 1342 | 1343 | // Here we're at the end - nomally we should never get that far 1344 | var declaringType = methodBase.DeclaringType; 1345 | if (declaringType == null) 1346 | { 1347 | break; 1348 | } 1349 | 1350 | // Get class name and method of the current stack frame 1351 | result = $"{declaringType.FullName}.{methodBase.Name}"; 1352 | 1353 | // Here, we're at the first method outside of SimpleLog class. 1354 | // This is the method that called the log method. We're done unless it is 1355 | // specified to skip additional frames and go further up the stack trace. 1356 | if (declaringType != typeof(SimpleLog) && --framesToSkip < 0) 1357 | { 1358 | break; 1359 | } 1360 | } 1361 | 1362 | return result; 1363 | } 1364 | } 1365 | } -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # .NET Desktop 2 | # Build and run tests for .NET Desktop or Windows classic desktop solutions. 3 | # Add steps that publish symbols, save build artifacts, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net 5 | 6 | pool: 7 | vmImage: 'VS2017-Win2016' 8 | 9 | variables: 10 | solution: '**/*.sln' 11 | buildPlatform: 'Any CPU' 12 | buildConfiguration: 'Release' 13 | 14 | steps: 15 | - task: NuGetToolInstaller@0 16 | 17 | - task: NuGetCommand@2 18 | inputs: 19 | restoreSolution: '$(solution)' 20 | 21 | - task: VSBuild@1 22 | inputs: 23 | solution: '$(solution)' 24 | platform: '$(buildPlatform)' 25 | configuration: '$(buildConfiguration)' 26 | 27 | - task: VSTest@2 28 | inputs: 29 | platform: '$(buildPlatform)' 30 | configuration: '$(buildConfiguration)' 31 | -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | --------------------------------------------------------------------------------