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
23 #define BUFFSIZE 8192U
24 #define MAX_THREADS MAXIMUM_WAIT_OBJECTS
26 // --------------------------------------------------------------------------
28 // --------------------------------------------------------------------------
30 static wchar_t to_lower(const wchar_t c
)
32 return ((c
>= L
'A') && (c
<= L
'Z')) ? (L
'a' + (c
- L
'A')) : c
;
35 static BOOL
is_terminal(const HANDLE handle
)
38 return GetConsoleMode(handle
, &mode
);
41 static DWORD
count_handles(const HANDLE
*const array
, const size_t maximum
)
44 for (counter
= 0U; counter
< maximum
; ++counter
)
55 static const wchar_t *get_filename(const wchar_t *filePath
)
57 for (const wchar_t *ptr
= filePath
; *ptr
!= L
'\0'; ++ptr
)
59 if ((*ptr
== L
'\\') || (*ptr
== L
'/'))
67 static BOOL
is_null_device(const wchar_t *filePath
)
69 filePath
= get_filename(filePath
);
70 if ((to_lower(filePath
[0U]) == L
'n') && (to_lower(filePath
[1U]) == L
'u') || (to_lower(filePath
[2U]) == L
'l'))
72 return ((filePath
[3U] == L
'\0') || (filePath
[3U] == L
'.'));
77 static wchar_t *concat_va(const wchar_t *const first
, ...)
84 for (ptr
= first
; ptr
!= NULL
; ptr
= va_arg(ap
, const wchar_t*))
90 wchar_t *const buffer
= (wchar_t*)LocalAlloc(LPTR
, sizeof(wchar_t) * (len
+ 1U));
94 for (ptr
= first
; ptr
!= NULL
; ptr
= va_arg(ap
, const wchar_t*))
96 lstrcatW(buffer
, ptr
);
104 #define CLOSE_HANDLE(HANDLE) do \
106 if (((HANDLE) != NULL) && ((HANDLE) != INVALID_HANDLE_VALUE)) \
108 CloseHandle((HANDLE)); \
114 #define CONCAT(...) concat_va(__VA_ARGS__, NULL)
116 // --------------------------------------------------------------------------
117 // Console CTRL+C handler
118 // --------------------------------------------------------------------------
120 static volatile BOOL g_stop
= FALSE
;
122 static BOOL WINAPI
console_handler(const DWORD ctrlType
)
127 case CTRL_BREAK_EVENT
:
128 case CTRL_CLOSE_EVENT
:
136 // --------------------------------------------------------------------------
138 // --------------------------------------------------------------------------
140 static ULONGLONG
get_version(void)
142 const HRSRC hVersion
= FindResourceW(NULL
, MAKEINTRESOURCE(VS_VERSION_INFO
), RT_VERSION
);
145 const HGLOBAL hResource
= LoadResource(NULL
, hVersion
);
148 const DWORD sizeOfResource
= SizeofResource(NULL
, hResource
);
149 if (sizeOfResource
>= sizeof(VS_FIXEDFILEINFO
))
151 const PVOID addrResourceBlock
= LockResource(hResource
);
152 if (addrResourceBlock
)
154 VS_FIXEDFILEINFO
*fileInfoData
;
156 if (VerQueryValueW(addrResourceBlock
, L
"\\", &fileInfoData
, &fileInfoSize
))
158 ULARGE_INTEGER fileVersion
;
159 fileVersion
.LowPart
= fileInfoData
->dwFileVersionLS
;
160 fileVersion
.HighPart
= fileInfoData
->dwFileVersionMS
;
161 return fileVersion
.QuadPart
;
171 static const wchar_t *create_version_string(void)
173 static wchar_t buffer
[64U];
174 const ULONGLONG version
= get_version();
177 DWORD_PTR args
[] = { (DWORD_PTR
)((version
>> 48) & 0xFFFF), (DWORD_PTR
)((version
>> 32) & 0xFFFF), (DWORD_PTR
)((version
>> 16) & 0xFFFF), (DWORD_PTR
)TEXT(__DATE__
) };
178 if (FormatMessageW(FORMAT_MESSAGE_ARGUMENT_ARRAY
| FORMAT_MESSAGE_FROM_STRING
, L
"tee for Windows v%1!u!.%2!u!.%3!u! [%4!s!]\n", 0U, 0U, buffer
, ARRAYSIZE(buffer
), (va_list*)args
))
184 return L
"tee for Windows\n";
187 // --------------------------------------------------------------------------
189 // --------------------------------------------------------------------------
191 static char *utf16_to_utf8(const wchar_t *const input
)
193 const int buff_size
= WideCharToMultiByte(CP_UTF8
, 0, input
, -1, NULL
, 0, NULL
, NULL
);
196 char *const buffer
= (char*)LocalAlloc(LPTR
, buff_size
);
199 const int result
= WideCharToMultiByte(CP_UTF8
, 0, input
, -1, buffer
, buff_size
, NULL
, NULL
);
200 if ((result
> 0) && (result
<= buff_size
))
210 static BOOL
write_text(const HANDLE handle
, const wchar_t *const text
)
214 if (GetConsoleMode(handle
, &written
))
216 result
= WriteConsoleW(handle
, text
, lstrlenW(text
), &written
, NULL
);
220 char *const utf8_text
= utf16_to_utf8(text
);
223 result
= WriteFile(handle
, utf8_text
, lstrlenA(utf8_text
), &written
, NULL
);
224 LocalFree(utf8_text
);
230 #define WRITE_TEXT(...) do \
232 wchar_t* const _message = CONCAT(__VA_ARGS__); \
235 write_text(hStdErr, _message); \
236 LocalFree(_message); \
241 // --------------------------------------------------------------------------
243 // --------------------------------------------------------------------------
247 HANDLE hOutput
, hError
;
252 static thread_t threadData
[MAX_THREADS
];
253 static BYTE buffer
[2U][BUFFSIZE
];
254 static DWORD bytesTotal
[2U] = { 0U, 0U }, pending
= 0U, index
= 0U;
255 static CRITICAL_SECTION criticalSection
;
256 static CONDITION_VARIABLE condIsReady
, condAllDone
;
258 static DWORD WINAPI
writer_thread_start_routine(const LPVOID lpThreadParameter
)
260 DWORD bytesWritten
, myIndex
= 0U;
261 const thread_t
* const param
= &threadData
[(DWORD_PTR
)lpThreadParameter
];
263 EnterCriticalSection(&criticalSection
);
267 while (index
== myIndex
)
269 if (!SleepConditionVariableCS(&condIsReady
, &criticalSection
, INFINITE
))
271 LeaveCriticalSection(&criticalSection
);
272 write_text(param
->hError
, L
"[tee] System error: Failed to sleep on conditional variable!\n");
278 LeaveCriticalSection(&criticalSection
);
280 if (myIndex
== MAXDWORD
)
285 for (DWORD offset
= 0U; offset
< bytesTotal
[myIndex
]; offset
+= bytesWritten
)
287 const BOOL result
= WriteFile(param
->hOutput
, buffer
[myIndex
] + offset
, bytesTotal
[myIndex
] - offset
, &bytesWritten
, NULL
);
288 if ((!result
) || (!bytesWritten
))
290 write_text(param
->hError
, L
"[tee] Error: Not all data could be written!\n");
295 EnterCriticalSection(&criticalSection
);
299 WakeConditionVariable(&condAllDone
);
304 LeaveCriticalSection(&criticalSection
);
305 FlushFileBuffers(param
->hOutput
);
306 EnterCriticalSection(&criticalSection
);
311 // --------------------------------------------------------------------------
313 // --------------------------------------------------------------------------
317 BOOL append
, flush
, ignore
, help
, version
;
321 #define PARSE_OPTION(SHRT, NAME) do \
323 if ((lc == L##SHRT) || (name && (lstrcmpiW(name, L#NAME) == 0))) \
325 options->NAME = TRUE; \
331 static BOOL
parse_option(options_t
*const options
, const wchar_t c
, const wchar_t *const name
)
333 const wchar_t lc
= to_lower(c
);
335 PARSE_OPTION('a', append
);
336 PARSE_OPTION('f', flush
);
337 PARSE_OPTION('i', ignore
);
338 PARSE_OPTION('h', help
);
339 PARSE_OPTION('v', version
);
344 static BOOL
parse_argument(options_t
*const options
, const wchar_t *const argument
)
346 if ((argument
[0U] != L
'-') || (argument
[1U] == L
'\0'))
351 if (argument
[1U] == L
'-')
353 return (argument
[2U] != L
'\0') && parse_option(options
, L
'\0', argument
+ 2U);
357 for (const wchar_t* ptr
= argument
+ 1U; *ptr
!= L
'\0'; ++ptr
)
359 if (!parse_option(options
, *ptr
, NULL
))
368 // --------------------------------------------------------------------------
370 // --------------------------------------------------------------------------
372 int wmain(const int argc
, const wchar_t *const argv
[])
374 HANDLE hThreads
[MAX_THREADS
], hMyFile
[MAX_THREADS
- 1U];
375 int exitCode
= 1, argOff
= 1;
376 DWORD fileCount
= 0U, threadCount
= 0U;
379 /* Initialize local variables */
380 SecureZeroMemory(hThreads
, sizeof(hThreads
));
381 SecureZeroMemory(&options
, sizeof(options_t
));
382 for (DWORD fileIndex
= 0U; fileIndex
< ARRAYSIZE(hMyFile
); ++fileIndex
)
384 hMyFile
[fileIndex
] = INVALID_HANDLE_VALUE
;
387 /* Initialize standard streams */
388 const HANDLE hStdIn
= GetStdHandle(STD_INPUT_HANDLE
), hStdOut
= GetStdHandle(STD_OUTPUT_HANDLE
), hStdErr
= GetStdHandle(STD_ERROR_HANDLE
);
389 if ((hStdIn
== INVALID_HANDLE_VALUE
) || (hStdOut
== INVALID_HANDLE_VALUE
) || (hStdErr
== INVALID_HANDLE_VALUE
))
394 /* Set up CRTL+C handler */
395 SetConsoleCtrlHandler(console_handler
, TRUE
);
397 /* Parse command-line options */
398 while ((argOff
< argc
) && (argv
[argOff
][0U] == L
'-') && (argv
[argOff
][1U] != L
'\0'))
400 const wchar_t *const argValue
= argv
[argOff
++];
401 if ((argValue
[1U] == L
'-') && (argValue
[2U] == L
'\0'))
405 else if (!parse_argument(&options
, argValue
))
407 WRITE_TEXT(L
"[tee] Error: Invalid option \"", argValue
, L
"\" encountered!\n");
412 /* Print version information */
415 write_text(hStdErr
, create_version_string());
419 /* Print manual page */
422 write_text(hStdErr
, create_version_string());
423 write_text(hStdErr
, L
"\n"
424 L
"Copy standard input to output file(s), and also to standard output.\n\n"
426 L
" gizmo.exe [...] | tee.exe [options] <file_1> ... <file_n>\n\n"
428 L
" -a --append Append to the existing file, instead of truncating\n"
429 L
" -f --flush Flush output file after each write operation\n"
430 L
" -i --ignore Ignore the interrupt signal (SIGINT), e.g. CTRL+C\n\n");
434 /* Check output file name */
437 write_text(hStdErr
, L
"[tee] Error: Output file name is missing. Type \"tee --help\" for details!\n");
441 /* Initialize critical section */
442 if (!InitializeCriticalSectionAndSpinCount(&criticalSection
, 4096U))
444 write_text(hStdErr
, L
"[tee] System error: Failed to initialize critical section!\n");
448 /* Initialize cond variables */
449 InitializeConditionVariable(&condIsReady
);
450 InitializeConditionVariable(&condAllDone
);
452 /* Open output file(s) */
453 while ((argOff
< argc
) && (fileCount
< ARRAYSIZE(hMyFile
)))
455 const wchar_t* const fileName
= argv
[argOff
++];
456 if (!is_null_device(fileName
))
458 const HANDLE hFile
= CreateFileW(fileName
, GENERIC_WRITE
, FILE_SHARE_READ
, NULL
, options
.append
? OPEN_ALWAYS
: CREATE_ALWAYS
, 0U, NULL
);
459 if ((hMyFile
[fileCount
++] = hFile
) == INVALID_HANDLE_VALUE
)
461 WRITE_TEXT(L
"[tee] Error: Failed to open the output file \"", fileName
, L
"\" for writing!\n");
464 else if (options
.append
)
466 LARGE_INTEGER offset
= { .QuadPart
= 0LL };
467 if (!SetFilePointerEx(hFile
, offset
, NULL
, FILE_END
))
469 write_text(hStdErr
, L
"[tee] Error: Failed to move the file pointer to the end of the file!\n");
476 /* Check output file name */
479 write_text(hStdErr
, L
"[tee] Warning: Too many input files, ignoring excess files!\n");
482 /* Determine number of outputs */
483 const DWORD outputCount
= fileCount
+ 1U;
486 for (DWORD threadId
= 0; threadId
< outputCount
; ++threadId
)
488 threadData
[threadId
].hOutput
= (threadId
> 0U) ? hMyFile
[threadId
- 1U] : hStdOut
;
489 threadData
[threadId
].hError
= hStdErr
;
490 threadData
[threadId
].flush
= options
.flush
&& (!is_terminal(threadData
[threadId
].hOutput
));
491 if (!(hThreads
[threadCount
++] = CreateThread(NULL
, 0U, writer_thread_start_routine
, (LPVOID
)(DWORD_PTR
)threadId
, 0U, NULL
)))
493 write_text(hStdErr
, L
"[tee] System error: Failed to create thread!\n");
498 /* Are we reading from a pipe? */
499 const BOOL isPipeInput
= (GetFileType(hStdIn
) == FILE_TYPE_PIPE
);
501 /* Initialize the index */
504 /* Process all input from STDIN stream */
507 if (!ReadFile(hStdIn
, buffer
[myIndex
], BUFFSIZE
, &bytesTotal
[myIndex
], NULL
))
509 if (GetLastError() != ERROR_BROKEN_PIPE
)
511 write_text(hStdErr
, L
"[tee] Error: Failed to read input data!\n");
517 if ((!bytesTotal
[myIndex
]) && (!isPipeInput
)) /*pipes may return zero bytes, even when more data can become available later!*/
522 EnterCriticalSection(&criticalSection
);
526 if (!SleepConditionVariableCS(&condAllDone
, &criticalSection
, INFINITE
))
528 LeaveCriticalSection(&criticalSection
);
529 write_text(hStdErr
, L
"[tee] System error: Failed to sleep on conditional variable!\n");
534 pending
= threadCount
;
536 myIndex
= 1U - myIndex
;
538 LeaveCriticalSection(&criticalSection
);
539 WakeAllConditionVariable(&condIsReady
);
541 while ((!g_stop
) || options
.ignore
);
547 /* Stop the worker threads */
548 EnterCriticalSection(&criticalSection
);
550 LeaveCriticalSection(&criticalSection
);
551 WakeAllConditionVariable(&condIsReady
);
553 /* Wait for worker threads to exit */
554 const DWORD pendingThreads
= count_handles(hThreads
, ARRAYSIZE(hThreads
));
555 if (pendingThreads
> 0U)
557 const DWORD result
= WaitForMultipleObjects(pendingThreads
, hThreads
, TRUE
, 10000U);
558 if (!((result
>= WAIT_OBJECT_0
) && (result
< WAIT_OBJECT_0
+ pendingThreads
)))
560 for (DWORD threadId
= 0U; threadId
< pendingThreads
; ++threadId
)
562 if (WaitForSingleObject(hThreads
[threadId
], 16U) != WAIT_OBJECT_0
)
564 write_text(hStdErr
, L
"[tee] Error: Worker thread did not exit cleanly!\n");
565 TerminateThread(hThreads
[threadId
], 1U);
571 /* Flush the output file */
574 for (size_t fileIndex
= 0U; fileIndex
< ARRAYSIZE(hMyFile
); ++fileIndex
)
576 if (hMyFile
[fileIndex
] != INVALID_HANDLE_VALUE
)
578 FlushFileBuffers(hMyFile
[fileIndex
]);
583 /* Close worker threads */
584 for (DWORD threadId
= 0U; threadId
< ARRAYSIZE(hThreads
); ++threadId
)
586 CLOSE_HANDLE(hThreads
[threadId
]);
589 /* Close the output file(s) */
590 for (size_t fileIndex
= 0U; fileIndex
< ARRAYSIZE(hMyFile
); ++fileIndex
)
592 CLOSE_HANDLE(hMyFile
[fileIndex
]);
595 /* Delete critical section */
596 DeleteCriticalSection(&criticalSection
);
602 // --------------------------------------------------------------------------
604 // --------------------------------------------------------------------------
607 #pragma warning(disable: 4702)
611 SetErrorMode(SEM_FAILCRITICALERRORS
);
614 LPWSTR
*const szArglist
= CommandLineToArgvW(GetCommandLineW(), &nArgs
);
617 ExitProcess((UINT
)-1);
620 const int retval
= wmain(nArgs
, szArglist
);
621 LocalFree(szArglist
);
622 ExitProcess((UINT
)retval
);