Added version info to the EXE file.
[tee-win32.git] / tee.c
blob2099d430790fbf364fc28e40156dfe4c07256bf2
1 /*
2 * CertViewer - tee for Windows
3 * Copyright (c) 2023 "dEajL3kA" <Cumpoing79@web.de>
5 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6 * associated documentation files (the "Software"), to deal in the Software without restriction,
7 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
8 * sub license, and/or sell copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions: The above copyright notice and this
10 * permission notice shall be included in all copies or substantial portions of the Software.
12 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
13 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
14 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
15 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
16 * OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 #define WIN32_LEAN_AND_MEAN 1
19 #include <Windows.h>
20 #include <ShellAPI.h>
22 #define BUFFSIZE 4096U
23 #pragma warning(disable: 4706)
25 // --------------------------------------------------------------------------
26 // Utilities
27 // --------------------------------------------------------------------------
29 static BOOL is_terminal(const HANDLE handle)
31 DWORD mode;
32 return GetConsoleMode(handle, &mode);
35 #define APPEND_STRING(X) \
36 do { lstrcpyW(ptr, str##X); ptr += len##X; } while(0)
38 static wchar_t *concat_3(const wchar_t *const strA, const wchar_t *const strB, const wchar_t *const strC)
40 const size_t lenA = lstrlenW(strA), lenB = lstrlenW(strB), lenC = lstrlenW(strC);
41 wchar_t *const buffer = (wchar_t*) LocalAlloc(LPTR, sizeof(wchar_t) * (lenA + lenB + lenC + 1U));
42 if (buffer)
44 wchar_t *ptr = buffer;
45 APPEND_STRING(A);
46 APPEND_STRING(B);
47 APPEND_STRING(C);
49 return buffer;
52 #define SAFE_CLOSE_HANDLE(HANDLE) do \
53 { \
54 if (HANDLE) \
55 { \
56 CloseHandle((HANDLE)); \
57 } \
58 } \
59 while (0)
61 // --------------------------------------------------------------------------
62 // Console CTRL+C handler
63 // --------------------------------------------------------------------------
65 static volatile BOOL g_stop = FALSE;
67 static BOOL WINAPI console_handler(const DWORD ctrlType)
69 switch (ctrlType)
71 case CTRL_C_EVENT:
72 case CTRL_BREAK_EVENT:
73 case CTRL_CLOSE_EVENT:
74 g_stop = TRUE;
75 return TRUE;
76 default:
77 return FALSE;
81 // --------------------------------------------------------------------------
82 // Version
83 // --------------------------------------------------------------------------
85 static ULONGLONG get_version(void)
87 const HRSRC hVersion = FindResourceW(NULL, MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
88 if (hVersion)
90 const HGLOBAL hResource = LoadResource(NULL, hVersion);
91 if (hResource)
93 const DWORD sizeOfResource = SizeofResource(NULL, hResource);
94 if (sizeOfResource >= sizeof(VS_FIXEDFILEINFO))
96 const PVOID addrResourceBlock = LockResource(hResource);
97 if (addrResourceBlock)
99 VS_FIXEDFILEINFO *fileInfoData;
100 UINT fileInfoSize;
101 if (VerQueryValueW(addrResourceBlock, L"\\", &fileInfoData, &fileInfoSize))
103 ULARGE_INTEGER fileVersion;
104 fileVersion.LowPart = fileInfoData->dwFileVersionLS;
105 fileVersion.HighPart = fileInfoData->dwFileVersionMS;
106 return fileVersion.QuadPart;
113 return 0U;
116 static const wchar_t *get_version_string(void)
118 static wchar_t text[64U] = { '\0' };
119 lstrcpyW(text, L"tee for Windows v#.#.# [" TEXT(__DATE__) L"]\n");
121 const ULONGLONG version = get_version();
122 if (version)
124 text[17U] = L'0' + ((version >> 48) & 0xFFFF);
125 text[19U] = L'0' + ((version >> 32) & 0xFFFF);
126 text[21U] = L'0' + ((version >> 16) & 0xFFFF);
129 return text;
132 // --------------------------------------------------------------------------
133 // Text output
134 // --------------------------------------------------------------------------
136 static char *utf16_to_utf8(const wchar_t *const input)
138 const int buff_size = WideCharToMultiByte(CP_UTF8, 0, input, -1, NULL, 0, NULL, NULL);
139 if (buff_size > 0)
141 char *const buffer = (char*)LocalAlloc(LPTR, buff_size);
142 if (buffer)
144 const int result = WideCharToMultiByte(CP_UTF8, 0, input, -1, buffer, buff_size, NULL, NULL);
145 if ((result > 0) && (result <= buff_size))
147 return buffer;
149 LocalFree(buffer);
152 return NULL;
155 static BOOL write_text(const HANDLE handle, const wchar_t *const text)
157 BOOL result = FALSE;
158 DWORD written;
159 if (GetConsoleMode(handle, &written))
161 result = WriteConsoleW(handle, text, lstrlenW(text), &written, NULL);
163 else
165 char *const utf8_text = utf16_to_utf8(text);
166 if (utf8_text)
168 result = WriteFile(handle, utf8_text, lstrlenA(utf8_text), &written, NULL);
169 LocalFree(utf8_text);
172 return result;
175 // --------------------------------------------------------------------------
176 // Writer thread
177 // --------------------------------------------------------------------------
179 static BYTE buffer[2U][BUFFSIZE];
180 static DWORD bytesTotal[2U] = { 0U, 0U };
181 static volatile ULONG_PTR index = 0U;
183 typedef struct
185 HANDLE hOutput,hError;
186 BOOL flush;
187 HANDLE hEventReady[2U], hEventCompleted;
189 thread_t;
191 static DWORD WINAPI writer_thread_start_routine(const LPVOID lpThreadParameter)
193 DWORD bytesWritten;
194 const thread_t *const param = (thread_t*) lpThreadParameter;
196 for (;;)
198 switch (WaitForMultipleObjects(2U, param->hEventReady, FALSE, INFINITE))
200 case WAIT_OBJECT_0:
201 break;
202 case WAIT_OBJECT_0 + 1U:
203 SetEvent(param->hEventCompleted);
204 return 0U;
205 default:
206 write_text(param->hError, L"[tee] System error: Failed to wait for event!\n");
207 return 1U;
210 const ULONG_PTR myIndex = index;
212 for (DWORD offset = 0U; offset < bytesTotal[myIndex]; offset += bytesWritten)
214 const BOOL result = WriteFile(param->hOutput, buffer[myIndex] + offset, bytesTotal[myIndex] - offset, &bytesWritten, NULL);
215 if ((!result) || (!bytesWritten))
217 write_text(param->hError, L"[tee] Error: Not all data could be written!\n");
218 break;
222 SetEvent(param->hEventCompleted);
224 if (param->flush)
226 FlushFileBuffers(param->hOutput);
231 // --------------------------------------------------------------------------
232 // MAIN
233 // --------------------------------------------------------------------------
235 int wmain(const int argc, const wchar_t *const argv[])
237 HANDLE hThreads[2U] = { NULL, NULL };
238 HANDLE hEventStop = NULL, hEventThrdReady[2U] = { NULL, NULL }, hEventCompleted[2U] = { NULL, NULL };
239 HANDLE hMyFile = INVALID_HANDLE_VALUE;
240 int exitCode = 1, argOff = 1;
241 BOOL append = FALSE, flush = FALSE, ignore = FALSE;
242 thread_t threadData[2U];
244 /* Initialize standard streams */
245 const HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE), hStdOut = GetStdHandle(STD_OUTPUT_HANDLE), hStdErr = GetStdHandle(STD_ERROR_HANDLE);
246 if ((hStdIn == INVALID_HANDLE_VALUE) || (hStdOut == INVALID_HANDLE_VALUE) || (hStdErr == INVALID_HANDLE_VALUE))
248 FatalExit(-1);
251 /* Set up CRTL+C handler */
252 SetConsoleCtrlHandler(console_handler, TRUE);
254 /* Print version */
255 if ((argc > 1) && (lstrcmpiW(argv[1], L"--version") == 0))
257 write_text(hStdErr, get_version_string());
258 return 0;
261 /* Print manpage */
262 if ((argc < 2) || (lstrcmpiW(argv[1], L"/?") == 0) || (lstrcmpiW(argv[1], L"--help") == 0))
264 write_text(hStdErr, get_version_string());
265 write_text(hStdErr, L"\n"
266 L"Usage:\n"
267 L" your_program.exe [...] | tee.exe [options] <output_file>\n\n"
268 L"Options:\n"
269 L" --append Append to the existing file, instead of truncating\n"
270 L" --flush Flush output file after each write operation\n"
271 L" --ignore Ignore the interrupt signal (SIGINT), e.g. CTRL+C\n\n");
272 return 1;
275 /* Parse command-line options */
276 while (argOff < argc)
278 if ((argv[argOff][0U] == L'-') && (argv[argOff][1U] == L'-'))
280 const wchar_t *const option = argv[argOff++] + 2U;
281 if (*option == L'\0')
283 break;
285 else if (lstrcmpiW(option, L"append") == 0)
287 append = TRUE;
289 else if (lstrcmpiW(option, L"flush") == 0)
291 flush = TRUE;
293 else if (lstrcmpiW(option, L"ignore") == 0)
295 ignore = TRUE;
297 else
299 wchar_t *const message = concat_3(L"[tee] Error: Invalid option \"--", option, L"\" encountered!\n");
300 if (message)
302 write_text(hStdErr, message);
303 LocalFree(message);
305 return 1;
308 else
310 break; /* stop option processing */
314 /* Check output file name */
315 if (argOff >= argc)
317 write_text(hStdErr, L"[tee] Error: Output file name is missing!\n");
318 return 1;
321 /* Create events */
322 if (!(hEventStop = CreateEventW(NULL, TRUE, FALSE, NULL)))
324 write_text(hStdErr, L"[tee] System error: Failed to create event!\n\n");
325 goto cleanup;
327 for (size_t i = 0U; i < 2U; ++i)
329 if (!(hEventThrdReady[i] = CreateEventW(NULL, FALSE, FALSE, NULL)))
331 write_text(hStdErr, L"[tee] System error: Failed to create event!\n\n");
332 goto cleanup;
334 if (!(hEventCompleted[i] = CreateEventW(NULL, FALSE, FALSE, NULL)))
336 write_text(hStdErr, L"[tee] System error: Failed to create event!\n\n");
337 goto cleanup;
341 /* Open output file */
342 if ((hMyFile = CreateFileW(argv[argOff], GENERIC_WRITE, FILE_SHARE_READ, NULL, append ? OPEN_ALWAYS : CREATE_ALWAYS, 0U, NULL)) == INVALID_HANDLE_VALUE)
344 write_text(hStdErr, L"[tee] Error: Failed to open the output file for writing!\n");
345 goto cleanup;
348 /* Seek to the end of the file */
349 if (append)
351 LARGE_INTEGER offset = { .QuadPart = 0LL };
352 if (!SetFilePointerEx(hMyFile, offset, NULL, FILE_END))
354 write_text(hStdErr, L"[tee] Error: Failed to move the file pointer to the end of the file!\n");
355 goto cleanup;
359 /* Set up thread data */
360 for (size_t i = 0; i < 2U; ++i)
362 threadData[i].hOutput = (i > 0U) ? hMyFile : hStdOut;
363 threadData[i].hError = hStdErr;
364 threadData[i].flush = flush && (!is_terminal(threadData[i].hOutput));
365 threadData[i].hEventReady[0U] = hEventThrdReady[i];
366 threadData[i].hEventReady[1U] = hEventStop;
367 threadData[i].hEventCompleted = hEventCompleted[i];
370 /* Start threads */
371 for (size_t i = 0; i < 2U; ++i)
373 if (!(hThreads[i] = CreateThread(NULL, 0U, writer_thread_start_routine, &threadData[i], 0U, NULL)))
375 write_text(hStdErr, L"[tee] System error: Failed to create thread!\n");
376 goto cleanup;
380 /* Are we reading from a pipe? */
381 const BOOL isPipeInput = (GetFileType(hStdIn) == FILE_TYPE_PIPE);
383 /* Initialize index */
384 ULONG_PTR myIndex = 1U - index;
386 /* Process all input from STDIN stream */
389 for (size_t i = 0U; i < 2U; ++i)
391 if (!SetEvent(hEventThrdReady[i]))
393 write_text(hStdErr, L"[tee] System error: Failed to signal event!\n");
394 goto cleanup;
398 if (!ReadFile(hStdIn, buffer[myIndex], BUFFSIZE, &bytesTotal[myIndex], NULL))
400 if (GetLastError() != ERROR_BROKEN_PIPE)
402 write_text(hStdErr, L"[tee] Error: Failed to read input data!\n");
403 goto cleanup;
405 break;
408 if ((!bytesTotal[myIndex]) && (!isPipeInput)) /*pipes may return zero bytes, even when more data can become available later!*/
410 break;
413 const DWORD waitResult = WaitForMultipleObjects(2U, hEventCompleted, TRUE, INFINITE);
414 if ((waitResult != WAIT_OBJECT_0) && (waitResult != WAIT_OBJECT_0 + 1U))
416 write_text(hStdErr, L"[tee] System error: Failed to wait for events!\n");
417 goto cleanup;
420 myIndex = (ULONG_PTR) InterlockedExchangePointer((PVOID*)&index, (PVOID)myIndex);
422 while ((!g_stop) || ignore);
424 exitCode = 0;
426 cleanup:
428 /* Stop the worker threads */
429 if (hEventStop)
431 SetEvent(hEventStop);
434 /* Wait for worker threads to exit */
435 if (hThreads[0U])
437 const DWORD waitResult = WaitForMultipleObjects(hThreads[1U] ? 2U : 1U, hThreads, TRUE, 12000U);
438 if ((waitResult != WAIT_OBJECT_0) && (waitResult != WAIT_OBJECT_0 + 1U))
440 for (DWORD i = 0U; i < (hThreads[1U] ? 2U : 1U); ++i)
442 if (WaitForSingleObject(hThreads[i], 125U) != WAIT_OBJECT_0)
444 write_text(hStdErr, L"[tee] Error: Worker thread did not exit cleanly!\n");
445 TerminateThread(hThreads[i], 1U);
451 /* Close worker threads */
452 for (DWORD i = 0U; i < 2U; ++i)
454 SAFE_CLOSE_HANDLE(hThreads[i]);
457 /* Close output file */
458 if (hMyFile != INVALID_HANDLE_VALUE)
460 if (flush)
462 FlushFileBuffers(hMyFile);
464 CloseHandle(hMyFile);
467 /* Close events */
468 for (size_t i = 0U; i < 2U; ++i)
470 SAFE_CLOSE_HANDLE(hEventThrdReady[i]);
471 SAFE_CLOSE_HANDLE(hEventCompleted[i]);
473 SAFE_CLOSE_HANDLE(hEventStop);
475 /* Exit */
476 return exitCode;
479 // --------------------------------------------------------------------------
480 // CRT Startup
481 // --------------------------------------------------------------------------
483 #pragma warning(disable: 4702)
485 int wmainCRTStartup(void)
487 SetErrorMode(SEM_FAILCRITICALERRORS);
489 int nArgs;
490 LPWSTR *const szArglist = CommandLineToArgvW(GetCommandLineW(), &nArgs);
491 if (!szArglist)
493 FatalExit(-1);
496 const int retval = wmain(nArgs, szArglist);
497 LocalFree(szArglist);
498 ExitProcess((UINT)retval);
500 return 0;