Updated copyright year.
[tee-win32.git] / tee.c
blob65ac5077becbbb25904c47f14c51fbdf9a83a9c7
1 /*
2 * tee for Windows
3 * Copyright (c) 2024 "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>
21 #include <intrin.h>
22 #include <stdarg.h>
23 #include "include/cpu.h"
24 #include "include/version.h"
26 #pragma intrinsic(_InterlockedIncrement, _InterlockedDecrement)
28 #define BUFFER_SIZE (PROCESSOR_BITNESS * 128U)
29 #define BUFFERS 3U
30 #define MAX_THREADS MAXIMUM_WAIT_OBJECTS
32 // --------------------------------------------------------------------------
33 // Assertions
34 // --------------------------------------------------------------------------
36 #ifndef NDEBUG
37 #define ASSERT(CONDIATION, HANDLE_OUT, MESSAGE) do { \
38 static const wchar_t *const _message = L"[tee] Assertion Failed: " MESSAGE L"\n"; \
39 if (!(CONDIATION)) { \
40 write_text((HANDLE_OUT), _message); \
41 FatalExit(-1); \
42 } \
43 } while(0)
44 #else
45 #define ASSERT(CONDIATION, HANDLE_OUT, MESSAGE) ((void)0)
46 #endif
48 // --------------------------------------------------------------------------
49 // Utilities
50 // --------------------------------------------------------------------------
52 static wchar_t to_lower(const wchar_t c)
54 return ((c >= L'A') && (c <= L'Z')) ? (L'a' + (c - L'A')) : c;
57 static BOOL is_terminal(const HANDLE handle)
59 DWORD mode;
60 return GetConsoleMode(handle, &mode);
63 static DWORD count_handles(const HANDLE *const array, const size_t maximum)
65 DWORD counter;
66 for (counter = 0U; counter < maximum; ++counter)
68 if (!array[counter])
70 break;
74 return counter;
77 static const wchar_t *get_filename(const wchar_t *filePath)
79 for (const wchar_t *ptr = filePath; *ptr != L'\0'; ++ptr)
81 if ((*ptr == L'\\') || (*ptr == L'/'))
83 filePath = ptr + 1U;
87 return filePath;
90 static BOOL is_null_device(const wchar_t *filePath)
92 filePath = get_filename(filePath);
93 if ((to_lower(filePath[0U]) == L'n') && (to_lower(filePath[1U]) == L'u') && (to_lower(filePath[2U]) == L'l'))
95 return ((filePath[3U] == L'\0') || (filePath[3U] == L'.'));
98 return FALSE;
101 static wchar_t *format_string(const wchar_t *const format, ...)
103 wchar_t* buffer = NULL;
104 va_list ap;
106 va_start(ap, format);
107 const DWORD result = FormatMessageW(FORMAT_MESSAGE_FROM_STRING | FORMAT_MESSAGE_ALLOCATE_BUFFER, format, 0U, 0U, (LPWSTR)&buffer, 1U, &ap);
108 va_end(ap);
110 return result ? buffer : NULL;
113 static wchar_t *concat_va(const wchar_t *const first, ...)
115 const wchar_t *ptr;
116 va_list ap;
118 va_start(ap, first);
119 size_t len = 0U;
120 for (ptr = first; ptr != NULL; ptr = va_arg(ap, const wchar_t*))
122 len = lstrlenW(ptr);
124 va_end(ap);
126 wchar_t *const buffer = (wchar_t*)LocalAlloc(LPTR, sizeof(wchar_t) * (len + 1U));
127 if (buffer)
129 va_start(ap, first);
130 for (ptr = first; ptr != NULL; ptr = va_arg(ap, const wchar_t*))
132 lstrcatW(buffer, ptr);
134 va_end(ap);
137 return buffer;
140 #define CONCAT(...) concat_va(__VA_ARGS__, NULL)
142 #define VALID_HANDLE(HANDLE) (((HANDLE) != NULL) && ((HANDLE) != INVALID_HANDLE_VALUE))
144 #define FILL_ARRAY(ARRAY, VALUE) do \
146 for (size_t _index = 0U; _index < ARRAYSIZE(ARRAY); ++_index) \
148 ARRAY[_index] = (VALUE); \
151 while (0)
153 #define CLOSE_HANDLE(HANDLE) do \
155 if (VALID_HANDLE(HANDLE)) \
157 CloseHandle((HANDLE)); \
158 (HANDLE) = NULL; \
161 while (0)
163 // --------------------------------------------------------------------------
164 // Console CTRL+C handler
165 // --------------------------------------------------------------------------
167 static volatile BOOL g_stop = FALSE;
169 static BOOL WINAPI console_handler(const DWORD ctrlType)
171 switch (ctrlType)
173 case CTRL_C_EVENT:
174 case CTRL_BREAK_EVENT:
175 case CTRL_CLOSE_EVENT:
176 g_stop = TRUE;
177 return TRUE;
178 default:
179 return FALSE;
183 // --------------------------------------------------------------------------
184 // Text output
185 // --------------------------------------------------------------------------
187 static char *utf16_to_utf8(const wchar_t *const input)
189 const int buff_size = WideCharToMultiByte(CP_UTF8, 0, input, -1, NULL, 0, NULL, NULL);
190 if (buff_size > 0)
192 char *const buffer = (char*)LocalAlloc(LPTR, buff_size);
193 if (buffer)
195 const int result = WideCharToMultiByte(CP_UTF8, 0, input, -1, buffer, buff_size, NULL, NULL);
196 if ((result > 0) && (result <= buff_size))
198 return buffer;
200 LocalFree(buffer);
204 return NULL;
207 static BOOL write_text(const HANDLE handle, const wchar_t *const text)
209 BOOL result = FALSE;
210 DWORD written;
212 if (GetConsoleMode(handle, &written))
214 result = WriteConsoleW(handle, text, lstrlenW(text), &written, NULL);
216 else
218 char *const utf8_text = utf16_to_utf8(text);
219 if (utf8_text)
221 result = WriteFile(handle, utf8_text, lstrlenA(utf8_text), &written, NULL);
222 LocalFree(utf8_text);
226 return result;
229 #define WRITE_TEXT(...) do \
231 wchar_t* const _message = CONCAT(__VA_ARGS__); \
232 if (_message) \
234 write_text(hStdErr, _message); \
235 LocalFree(_message); \
238 while (0)
240 // --------------------------------------------------------------------------
241 // Condition variables
242 // --------------------------------------------------------------------------
244 static __forceinline void sleep_condvar_srw(const HANDLE hStdErr, const PCONDITION_VARIABLE condvar, const PSRWLOCK lock, const DWORD timeout, const BOOL sharedMode)
246 const ULONG flags = sharedMode ? CONDITION_VARIABLE_LOCKMODE_SHARED : 0U;
247 if (!SleepConditionVariableSRW(condvar, lock, timeout, flags))
249 if (GetLastError() != ERROR_TIMEOUT)
251 write_text(hStdErr, L"[tee] Operating system error: SleepConditionVariableSRW() has failed!\n");
252 TerminateProcess(GetCurrentProcess(), 1U);
257 // --------------------------------------------------------------------------
258 // Writer thread
259 // --------------------------------------------------------------------------
261 #define INCREMENT_INDEX(INDEX, FLAG) do \
263 if (++(INDEX) >= BUFFERS) \
265 (INDEX) = 0U; \
266 (FLAG) = (!(FLAG)); \
269 while (0)
271 typedef struct _thread
273 HANDLE hOutput, hError;
274 BOOL flush;
276 thread_t;
278 static BYTE g_buffer[BUFFERS][BUFFER_SIZE];
279 static DWORD g_bytesTotal[BUFFERS] = { 0U, 0U, 0U };
280 static volatile LONG g_pending[BUFFERS] = { 0L, 0L, 0L };
281 static SRWLOCK g_rwLocks[BUFFERS];
282 static CONDITION_VARIABLE g_condIsReady[BUFFERS], g_condAllDone[BUFFERS];
284 static DWORD WINAPI writer_thread_start_routine(const LPVOID lpThreadParameter)
286 DWORD bytesWritten = 0U, myIndex = 0U;
287 LONG pending = 0L;
288 BOOL myFlag = TRUE, writeErrors = FALSE;
289 PSRWLOCK rwLock = NULL;
290 const thread_t *const param = (const thread_t*)lpThreadParameter;
292 for (;;)
294 ASSERT(myIndex < BUFFERS, param->hError, L"Current buffer index is out of range!");
296 AcquireSRWLockShared(rwLock = &g_rwLocks[myIndex]);
298 pending = g_pending[myIndex];
300 while (!(myFlag ? (pending > 0L) : (pending < 0L)))
302 sleep_condvar_srw(param->hError, &g_condIsReady[myIndex], rwLock, INFINITE, TRUE);
303 pending = g_pending[myIndex];
306 const DWORD bytesTotal = g_bytesTotal[myIndex];
307 if (bytesTotal > BUFFER_SIZE)
309 ReleaseSRWLockShared(rwLock);
310 if (writeErrors)
312 write_text(param->hError, L"[tee] I/O error: Not all data could be written!\n");
314 return 0U;
317 for (DWORD offset = 0U; offset < bytesTotal; offset += bytesWritten)
319 const BOOL result = WriteFile(param->hOutput, g_buffer[myIndex] + offset, g_bytesTotal[myIndex] - offset, &bytesWritten, NULL);
320 if ((!result) || (!bytesWritten))
322 writeErrors = TRUE;
323 break;
327 ASSERT(g_pending > 0U, param->hError, L"Pending threads counter must be a positive value!");
329 pending = myFlag ? _InterlockedDecrement(&g_pending[myIndex]) : _InterlockedIncrement(&g_pending[myIndex]);
331 ReleaseSRWLockShared(rwLock);
333 if (!pending)
335 WakeConditionVariable(&g_condAllDone[myIndex]);
338 INCREMENT_INDEX(myIndex, myFlag);
340 if (param->flush)
342 FlushFileBuffers(param->hOutput);
347 // --------------------------------------------------------------------------
348 // Options
349 // --------------------------------------------------------------------------
351 typedef struct
353 BOOL append, buffer, delay, escape, flush, help, ignore, version;
355 options_t;
357 #define PARSE_OPTION(SHRT, NAME) do \
359 if ((lc == L##SHRT) || (name && (lstrcmpiW(name, L#NAME) == 0))) \
361 options->NAME = TRUE; \
362 return TRUE; \
365 while (0)
367 static BOOL parse_option(options_t *const options, const wchar_t c, const wchar_t *const name)
369 const wchar_t lc = to_lower(c);
371 PARSE_OPTION('a', append);
372 PARSE_OPTION('b', buffer);
373 PARSE_OPTION('d', delay);
374 PARSE_OPTION('e', escape);
375 PARSE_OPTION('f', flush);
376 PARSE_OPTION('h', help);
377 PARSE_OPTION('i', ignore);
378 PARSE_OPTION('v', version);
380 return FALSE;
383 static BOOL parse_argument(options_t *const options, const wchar_t *const argument)
385 if ((argument[0U] != L'-') || (argument[1U] == L'\0'))
387 return FALSE;
390 if (argument[1U] == L'-')
392 return (argument[2U] != L'\0') && parse_option(options, L'\0', argument + 2U);
394 else
396 for (const wchar_t* ptr = argument + 1U; *ptr != L'\0'; ++ptr)
398 if (!parse_option(options, *ptr, NULL))
400 return FALSE;
403 return TRUE;
407 // --------------------------------------------------------------------------
408 // Help screen
409 // --------------------------------------------------------------------------
411 static void print_helpscreen(const HANDLE hStdErr, const BOOL full)
413 wchar_t* const versionString = format_string(L"tee for Windows v%1!u!.%2!u!.%3!u! [%4!s!] [%5!s!]\n", APP_VERSION_MAJOR, APP_VERSION_MINOR, APP_VERSION_PATCH, PROCESSOR_ARCHITECTURE, TEXT(__DATE__));
414 write_text(hStdErr, versionString ? versionString : L"tee for Windows\n");
415 if (full)
417 write_text(hStdErr, L"\n"
418 L"Copy standard input to output file(s), and also to standard output.\n\n"
419 L"Usage:\n"
420 L" gizmo.exe [...] | tee.exe [options] <file_1> ... <file_n>\n\n"
421 L"Options:\n"
422 L" -a --append Append to the existing file, instead of truncating\n"
423 L" -b --buffer Enable write combining, i.e. buffer small chunks\n"
424 L" -e --escape Enable standard output ANSI escape code processing\n"
425 L" -f --flush Flush output file after each write operation\n"
426 L" -i --ignore Ignore the interrupt signal (SIGINT), e.g. CTRL+C\n"
427 L" -d --delay Add a small delay after each read operation\n\n");
429 if (versionString)
431 LocalFree(versionString);
435 // --------------------------------------------------------------------------
436 // MAIN
437 // --------------------------------------------------------------------------
439 int wmain(const int argc, const wchar_t *const argv[])
441 HANDLE hThreads[MAX_THREADS], hMyFiles[MAX_THREADS - 1U];
442 int exitCode = 1, argOff = 1;
443 BOOL myFlag = TRUE, readErrors = FALSE;
444 DWORD fileCount = 0U, threadCount = 0U, myIndex = 0U, bytesRead = 0U, totalBytes = 0U;
445 PSRWLOCK rwLock = NULL;
446 options_t options;
447 static thread_t threadData[MAX_THREADS];
449 /* Initialize local variables */
450 FILL_ARRAY(hMyFiles, INVALID_HANDLE_VALUE);
451 FILL_ARRAY(hThreads, NULL);
452 SecureZeroMemory(&options, sizeof(options));
453 SecureZeroMemory(&threadData, sizeof(threadData));
455 /* Initialize standard streams */
456 const HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE), hStdOut = GetStdHandle(STD_OUTPUT_HANDLE), hStdErr = GetStdHandle(STD_ERROR_HANDLE);
457 if (!(VALID_HANDLE(hStdIn) && VALID_HANDLE(hStdOut) && VALID_HANDLE(hStdErr)))
459 if (VALID_HANDLE(hStdErr))
461 write_text(hStdErr, L"[tee] Operating system error: GetStdHandle() has failed!\n");
463 return -1;
466 /* Initialize read/write locks and condition variables */
467 for (DWORD index = 0; index < BUFFERS; ++index)
469 InitializeSRWLock(&g_rwLocks[index]);
470 InitializeConditionVariable(&g_condIsReady[index]);
471 InitializeConditionVariable(&g_condAllDone[index]);
474 /* Set up CRTL+C handler */
475 SetConsoleCtrlHandler(console_handler, TRUE);
477 /* Parse command-line options */
478 while ((argOff < argc) && (argv[argOff][0U] == L'-') && (argv[argOff][1U] != L'\0'))
480 const wchar_t *const argValue= argv[argOff++];
481 if ((argValue[1U] == L'-') && (argValue[2U] == L'\0'))
483 break; /*stop!*/
485 else if (!parse_argument(&options, argValue))
487 WRITE_TEXT(L"[tee] Error: Invalid option \"", argValue, L"\" encountered!\n");
488 return 1;
492 /* Print manual page */
493 if (options.help || options.version)
495 print_helpscreen(hStdErr, options.help);
496 return 0;
499 /* Check output file name */
500 if (argOff >= argc)
502 write_text(hStdErr, L"[tee] Error: Output file name is missing. Type \"tee --help\" for details!\n");
503 return 1;
506 /* Determine input type */
507 const DWORD inputType = GetFileType(hStdIn);
508 if (inputType == FILE_TYPE_UNKNOWN)
510 if (GetLastError() != NO_ERROR)
512 write_text(hStdErr, L"[tee] Operating system error: GetFileType(hStdIn) has failed!\n");
513 return -1;
517 /* Enable ANSI escape code processing of stdout */
518 if (options.escape)
520 DWORD stdOutMode = 0U;
521 if (GetConsoleMode(hStdOut, &stdOutMode))
523 SetConsoleMode(hStdOut, stdOutMode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
527 /* Open output file(s) */
528 while ((argOff < argc) && (fileCount < ARRAYSIZE(hMyFiles)))
530 const wchar_t* const fileName = argv[argOff++];
531 if (!is_null_device(fileName))
533 const HANDLE hFile = CreateFileW(fileName, GENERIC_WRITE, FILE_SHARE_READ, NULL, options.append ? OPEN_ALWAYS : CREATE_ALWAYS, 0U, NULL);
534 if ((hMyFiles[fileCount++] = hFile) == INVALID_HANDLE_VALUE)
536 WRITE_TEXT(L"[tee] Error: Failed to open the output file \"", fileName, L"\" for writing!\n");
537 goto cleanUp;
539 else if (options.append)
541 LARGE_INTEGER offset = { .QuadPart = 0LL };
542 if (!SetFilePointerEx(hFile, offset, NULL, FILE_END))
544 write_text(hStdErr, L"[tee] Error: Failed to move the file pointer to the end of the file!\n");
545 goto cleanUp;
551 /* Check output file name */
552 if (argOff < argc)
554 write_text(hStdErr, L"[tee] Warning: Too many input files, ignoring excess files!\n");
557 /* Determine number of outputs */
558 const DWORD outputCount = fileCount + 1U;
560 /* Start threads */
561 for (DWORD threadId = 0; threadId < outputCount; ++threadId)
563 threadData[threadId].hOutput = (threadId > 0U) ? hMyFiles[threadId - 1U] : hStdOut;
564 threadData[threadId].hError = hStdErr;
565 threadData[threadId].flush = options.flush && (!is_terminal(threadData[threadId].hOutput));
566 if (!(hThreads[threadCount++] = CreateThread(NULL, 0U, writer_thread_start_routine, (LPVOID)&threadData[threadId], 0U, NULL)))
568 write_text(hStdErr, L"[tee] Operating system error: CreateThread() has failed!\n");
569 goto cleanUp;
573 /* Determine minumum chunk size */
574 const DWORD minimumLength = options.buffer ? (BUFFER_SIZE / 8U) : 1U;
576 /* Process all input from STDIN stream */
579 ASSERT(myIndex < BUFFERS, hStdErr, L"Current buffer index is out of range!");
581 AcquireSRWLockExclusive(rwLock = &g_rwLocks[myIndex]);
583 while (g_pending[myIndex])
585 sleep_condvar_srw(hStdErr, &g_condAllDone[myIndex], rwLock, INFINITE, FALSE);
588 BYTE *const ptrBuffer = g_buffer[myIndex];
590 for (totalBytes = 0U; totalBytes < minimumLength; totalBytes += bytesRead)
592 if (!ReadFile(hStdIn, &ptrBuffer[totalBytes], BUFFER_SIZE - totalBytes, &bytesRead, NULL))
594 if (GetLastError() != ERROR_BROKEN_PIPE)
596 readErrors = TRUE;
598 break;
600 if ((!bytesRead) && (inputType != FILE_TYPE_PIPE))
602 break; /*pipes may return zero bytes, even when more data can become available later!*/
606 if (!totalBytes)
608 ReleaseSRWLockExclusive(rwLock);
609 break;
612 g_bytesTotal[myIndex] = totalBytes;
613 g_pending[myIndex] = myFlag ? ((LONG)threadCount) : (-((LONG)threadCount));
615 ReleaseSRWLockExclusive(&g_rwLocks[myIndex]);
616 WakeAllConditionVariable(&g_condIsReady[myIndex]);
618 INCREMENT_INDEX(myIndex, myFlag);
620 if (readErrors)
622 break; /*abort on previous read errors*/
625 if (options.delay)
627 Sleep(1U);
630 while ((!g_stop) || options.ignore);
632 /* Check for read errors */
633 if (readErrors)
635 write_text(hStdErr, L"[tee] I/O error: Failed to read input data!\n");
636 goto cleanUp;
639 exitCode = 0;
641 cleanUp:
643 /* Wait for the pending writes */
644 AcquireSRWLockExclusive(&g_rwLocks[myIndex]);
645 while (g_pending[myIndex])
647 sleep_condvar_srw(hStdErr, &g_condAllDone[myIndex], &g_rwLocks[myIndex], 25000U, FALSE);
650 /* Shut down the remaining worker threads */
651 g_bytesTotal[myIndex] = MAXDWORD;
652 g_pending[myIndex] = myFlag ? MAXLONG : MINLONG;
653 ReleaseSRWLockExclusive(&g_rwLocks[myIndex]);
654 WakeAllConditionVariable(&g_condIsReady[myIndex]);
656 /* Wait for worker threads to exit */
657 const DWORD pendingThreads = count_handles(hThreads, ARRAYSIZE(hThreads));
658 if (pendingThreads > 0U)
660 const DWORD result = WaitForMultipleObjects(pendingThreads, hThreads, TRUE, 10000U);
661 if (!((result >= WAIT_OBJECT_0) && (result < WAIT_OBJECT_0 + pendingThreads)))
663 for (DWORD threadId = 0U; threadId < pendingThreads; ++threadId)
665 if (WaitForSingleObject(hThreads[threadId], 125U) != WAIT_OBJECT_0)
667 write_text(hStdErr, L"[tee] Internal error: Worker thread did not exit cleanly!\n");
668 TerminateThread(hThreads[threadId], 1U);
674 /* Flush the output file */
675 if (options.flush)
677 for (size_t fileIndex = 0U; fileIndex < ARRAYSIZE(hMyFiles); ++fileIndex)
679 if (hMyFiles[fileIndex] != INVALID_HANDLE_VALUE)
681 FlushFileBuffers(hMyFiles[fileIndex]);
686 /* Close worker threads */
687 for (DWORD threadId = 0U; threadId < ARRAYSIZE(hThreads); ++threadId)
689 CLOSE_HANDLE(hThreads[threadId]);
692 /* Close the output file(s) */
693 for (size_t fileIndex = 0U; fileIndex < ARRAYSIZE(hMyFiles); ++fileIndex)
695 CLOSE_HANDLE(hMyFiles[fileIndex]);
698 /* Exit */
699 return exitCode;
702 // --------------------------------------------------------------------------
703 // CRT Startup
704 // --------------------------------------------------------------------------
706 #ifndef _DEBUG
707 #pragma warning(disable: 4702)
709 int _startup(void)
711 SetErrorMode(SEM_FAILCRITICALERRORS);
713 int nArgs;
714 LPWSTR *const szArglist = CommandLineToArgvW(GetCommandLineW(), &nArgs);
715 if (!szArglist)
717 OutputDebugStringA("[tee-win32] System error: Failed to initialize command-line arguments!\n");
718 ExitProcess((UINT)-1);
721 const int retval = wmain(nArgs, szArglist);
722 LocalFree(szArglist);
723 ExitProcess((UINT)retval);
725 return 0;
728 #endif