├── .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 |
--------------------------------------------------------------------------------