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
23 #include "include/cpu.h"
24 #include "include/version.h"
26 #pragma intrinsic(_InterlockedIncrement, _InterlockedDecrement)
28 #define BUFFER_SIZE (PROCESSOR_BITNESS * 128U)
30 #define MAX_THREADS MAXIMUM_WAIT_OBJECTS
32 // --------------------------------------------------------------------------
34 // --------------------------------------------------------------------------
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); \
45 #define ASSERT(CONDIATION, HANDLE_OUT, MESSAGE) ((void)0)
48 // --------------------------------------------------------------------------
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
)
60 return GetConsoleMode(handle
, &mode
);
63 static DWORD
count_handles(const HANDLE
*const array
, const size_t maximum
)
66 for (counter
= 0U; counter
< maximum
; ++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
'/'))
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
'.'));
101 static wchar_t *format_string(const wchar_t *const format
, ...)
103 wchar_t* buffer
= NULL
;
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
);
110 return result
? buffer
: NULL
;
113 static wchar_t *concat_va(const wchar_t *const first
, ...)
120 for (ptr
= first
; ptr
!= NULL
; ptr
= va_arg(ap
, const wchar_t*))
126 wchar_t *const buffer
= (wchar_t*)LocalAlloc(LPTR
, sizeof(wchar_t) * (len
+ 1U));
130 for (ptr
= first
; ptr
!= NULL
; ptr
= va_arg(ap
, const wchar_t*))
132 lstrcatW(buffer
, ptr
);
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); \
153 #define CLOSE_HANDLE(HANDLE) do \
155 if (VALID_HANDLE(HANDLE)) \
157 CloseHandle((HANDLE)); \
163 // --------------------------------------------------------------------------
164 // Console CTRL+C handler
165 // --------------------------------------------------------------------------
167 static volatile BOOL g_stop
= FALSE
;
169 static BOOL WINAPI
console_handler(const DWORD ctrlType
)
174 case CTRL_BREAK_EVENT
:
175 case CTRL_CLOSE_EVENT
:
183 // --------------------------------------------------------------------------
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
);
192 char *const buffer
= (char*)LocalAlloc(LPTR
, buff_size
);
195 const int result
= WideCharToMultiByte(CP_UTF8
, 0, input
, -1, buffer
, buff_size
, NULL
, NULL
);
196 if ((result
> 0) && (result
<= buff_size
))
207 static BOOL
write_text(const HANDLE handle
, const wchar_t *const text
)
212 if (GetConsoleMode(handle
, &written
))
214 result
= WriteConsoleW(handle
, text
, lstrlenW(text
), &written
, NULL
);
218 char *const utf8_text
= utf16_to_utf8(text
);
221 result
= WriteFile(handle
, utf8_text
, lstrlenA(utf8_text
), &written
, NULL
);
222 LocalFree(utf8_text
);
229 #define WRITE_TEXT(...) do \
231 wchar_t* const _message = CONCAT(__VA_ARGS__); \
234 write_text(hStdErr, _message); \
235 LocalFree(_message); \
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 // --------------------------------------------------------------------------
259 // --------------------------------------------------------------------------
261 #define INCREMENT_INDEX(INDEX, FLAG) do \
263 if (++(INDEX) >= BUFFERS) \
266 (FLAG) = (!(FLAG)); \
271 typedef struct _thread
273 HANDLE hOutput
, hError
;
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;
288 BOOL myFlag
= TRUE
, writeErrors
= FALSE
;
289 PSRWLOCK rwLock
= NULL
;
290 const thread_t
*const param
= (const thread_t
*)lpThreadParameter
;
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
);
312 write_text(param
->hError
, L
"[tee] I/O error: Not all data could be written!\n");
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
))
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
);
335 WakeConditionVariable(&g_condAllDone
[myIndex
]);
338 INCREMENT_INDEX(myIndex
, myFlag
);
342 FlushFileBuffers(param
->hOutput
);
347 // --------------------------------------------------------------------------
349 // --------------------------------------------------------------------------
353 BOOL append
, buffer
, delay
, escape
, flush
, help
, ignore
, version
;
357 #define PARSE_OPTION(SHRT, NAME) do \
359 if ((lc == L##SHRT) || (name && (lstrcmpiW(name, L#NAME) == 0))) \
361 options->NAME = TRUE; \
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
);
383 static BOOL
parse_argument(options_t
*const options
, const wchar_t *const argument
)
385 if ((argument
[0U] != L
'-') || (argument
[1U] == L
'\0'))
390 if (argument
[1U] == L
'-')
392 return (argument
[2U] != L
'\0') && parse_option(options
, L
'\0', argument
+ 2U);
396 for (const wchar_t* ptr
= argument
+ 1U; *ptr
!= L
'\0'; ++ptr
)
398 if (!parse_option(options
, *ptr
, NULL
))
407 // --------------------------------------------------------------------------
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");
417 write_text(hStdErr
, L
"\n"
418 L
"Copy standard input to output file(s), and also to standard output.\n\n"
420 L
" gizmo.exe [...] | tee.exe [options] <file_1> ... <file_n>\n\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");
431 LocalFree(versionString
);
435 // --------------------------------------------------------------------------
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
;
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");
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'))
485 else if (!parse_argument(&options
, argValue
))
487 WRITE_TEXT(L
"[tee] Error: Invalid option \"", argValue
, L
"\" encountered!\n");
492 /* Print manual page */
493 if (options
.help
|| options
.version
)
495 print_helpscreen(hStdErr
, options
.help
);
499 /* Check output file name */
502 write_text(hStdErr
, L
"[tee] Error: Output file name is missing. Type \"tee --help\" for details!\n");
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");
517 /* Enable ANSI escape code processing of stdout */
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");
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");
551 /* Check output file name */
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;
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");
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
)
600 if ((!bytesRead
) && (inputType
!= FILE_TYPE_PIPE
))
602 break; /*pipes may return zero bytes, even when more data can become available later!*/
608 ReleaseSRWLockExclusive(rwLock
);
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
);
622 break; /*abort on previous read errors*/
630 while ((!g_stop
) || options
.ignore
);
632 /* Check for read errors */
635 write_text(hStdErr
, L
"[tee] I/O error: Failed to read input data!\n");
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 */
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
]);
702 // --------------------------------------------------------------------------
704 // --------------------------------------------------------------------------
707 #pragma warning(disable: 4702)
711 SetErrorMode(SEM_FAILCRITICALERRORS
);
714 LPWSTR
*const szArglist
= CommandLineToArgvW(GetCommandLineW(), &nArgs
);
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
);