From 15f49ba18a7cdf2445bd323905f1a480a259ac2a Mon Sep 17 00:00:00 2001 From: Corinna Vinschen Date: Tue, 21 Apr 2026 17:22:08 +0200 Subject: [PATCH 001/102] Cygwin: bump dll minor version to 3.6.10 Signed-off-by: Corinna Vinschen --- winsup/cygwin/include/cygwin/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winsup/cygwin/include/cygwin/version.h b/winsup/cygwin/include/cygwin/version.h index e61fd3913a..8c454e4485 100644 --- a/winsup/cygwin/include/cygwin/version.h +++ b/winsup/cygwin/include/cygwin/version.h @@ -11,7 +11,7 @@ details. */ changes to the DLL and is mainly informative in nature. */ #define CYGWIN_VERSION_DLL_MAJOR 3006 -#define CYGWIN_VERSION_DLL_MINOR 9 +#define CYGWIN_VERSION_DLL_MINOR 10 /* CYGWIN_VERSION_DLL_COMBINED gives us a single number representing the combined DLL major and minor numbers. */ From 08b5ac7cab4b07e190cac4fa4eb221a2e6cab639 Mon Sep 17 00:00:00 2001 From: Corinna Vinschen Date: Tue, 21 Apr 2026 19:39:02 +0200 Subject: [PATCH 002/102] Cygwin: rename release/3.6.8 to release/3.6.9 Skipping the 3.6.8 release because I screwed up. Signed-off-by: Corinna Vinschen (cherry picked from commit 8f1d2b499bc9e8007321090c1859e55c7ce7d417) --- winsup/cygwin/release/{3.6.8 => 3.6.9} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename winsup/cygwin/release/{3.6.8 => 3.6.9} (100%) diff --git a/winsup/cygwin/release/3.6.8 b/winsup/cygwin/release/3.6.9 similarity index 100% rename from winsup/cygwin/release/3.6.8 rename to winsup/cygwin/release/3.6.9 From d541ca756ac14f874ed12a1c416d13dc9f7e4c87 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Thu, 21 May 2026 16:36:20 +0900 Subject: [PATCH 003/102] Cygwin: console: Fix deadlock in console teardown that arises from pcon When a console process originating from a pseudo console exits, the current sequence is as follows: 1) atexit handlers (pcon_handover_proc) called. This also closes parent_pty_input_mutex which is introduced by the commit c4fb720afcf1. 2) close_all_files() is called via _exit(). This terminates cons_master_thread. parent_pty_input_mutex is referenced in cons_master_thread, so cons_master_thread may still use the mutex after it has been closed. This can lead to undesired behaviour, including a deadlock. Instead of registering pcon_hand_over_proc() as an atexit handler, this patch calls pcon_handover_proc() at a point in fhandler_console::close where cons_master_thread has already terminated, ensuring that no other thread accesses the mutex. Addresses: https://github.com/msys2/msys2-runtime/issues/338 Fixes: c4fb720afcf1 ("Cygwin: console: Use input_mutex in the parent PTY in master thread") Signed-off-by: Takashi Yano Reviewed-by: Johannes Schindelin (cherry picked from commit 9a360d364bb8f20cc7ec7567d2d136ca5d8e2454) --- winsup/cygwin/fhandler/console.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/winsup/cygwin/fhandler/console.cc b/winsup/cygwin/fhandler/console.cc index 6220a9142f..224177bd54 100644 --- a/winsup/cygwin/fhandler/console.cc +++ b/winsup/cygwin/fhandler/console.cc @@ -1954,7 +1954,6 @@ fhandler_console::setup_pcon_hand_over () if (get_console_process_id (owner, true, false, false, false)) { inside_pcon = true; - atexit (fhandler_console::pcon_hand_over_proc); parent_pty = i; parent_pty_input_mutex = cygwin_shared->tty[i]->open_input_mutex (MAXIMUM_ALLOWED); @@ -2092,6 +2091,8 @@ fhandler_console::close (int flag) CloseHandle (output_mutex); output_mutex = NULL; + pcon_hand_over_proc (); + WaitForSingleObject (shared_info_mutex, INFINITE); if (--shared_info_state[unit] == 0 && shared_console_info[unit]) { From 231c2b90b848aec4db9718c4c5b4747d2f186e25 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Tue, 26 May 2026 18:40:30 +0900 Subject: [PATCH 004/102] Cygwin: console: Fix handling of surrogate pairs The commit 782aac590af7 introduced surrogate-pair handling. However, it does not work as expected in the legacy console. This is because, in legacy console, a KeyDown event for ALT key with UnicodeChar == 0 is inserted between the surrogate pair. The current code reads the next input event unconditionally for the second UnicodeChar, but it is not correct. This patch searches the next appropriate key event with a valid UnicodeChar, ensuring that the second code unit is valid. Fixes: 782aac590af7 ("Cygwin: console: Handle Unicode surrogate pairs.") Signed-off-by: Takashi Yano Reviewed-by: Johannes Schindelin (cherry picked from commit 1ff8990c0b8918c9ecad96314efa3f580a5e575c) --- winsup/cygwin/fhandler/console.cc | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/winsup/cygwin/fhandler/console.cc b/winsup/cygwin/fhandler/console.cc index 224177bd54..ca32d7faa7 100644 --- a/winsup/cygwin/fhandler/console.cc +++ b/winsup/cygwin/fhandler/console.cc @@ -1399,9 +1399,21 @@ fhandler_console::process_input_message (void) } else { - WCHAR second = unicode_char >= 0xd800 && unicode_char <= 0xdbff - && i + 1 < total_read ? - input_rec[i + 1].Event.KeyEvent.uChar.UnicodeChar : 0; + WCHAR second = 0; + DWORD second_pos = i; + if (unicode_char >= 0xd800 && unicode_char <= 0xdbff) + for (DWORD j = i + 1; j < total_read; j++) + { + /* Do not check bKeyDown. bKeyDown is 0 for surrogate + pair in legacy console */ + if (input_rec[j].EventType == KEY_EVENT && + input_rec[j].Event.KeyEvent.uChar.UnicodeChar) + { + second = input_rec[j].Event.KeyEvent.uChar.UnicodeChar; + second_pos = j; + break; + } + } if (second < 0xdc00 || second > 0xdfff) { @@ -1412,7 +1424,7 @@ fhandler_console::process_input_message (void) /* handle surrogate pairs */ WCHAR pair[2] = { unicode_char, second }; nread = sys_wcstombs (tmp + 1, 59, pair, 2); - i++; + i = second_pos; } /* Determine if the keystroke is modified by META. The tricky From 9a768521774b13cf8178282deba2ce35a8f3a78b Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Fri, 29 May 2026 11:27:55 +0900 Subject: [PATCH 005/102] Add recent two fixes into release note (cherry picked from commit e3cf6a9302d0cfc28eaad7556b0ce739b8641ae8) --- winsup/cygwin/release/3.6.10 | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 winsup/cygwin/release/3.6.10 diff --git a/winsup/cygwin/release/3.6.10 b/winsup/cygwin/release/3.6.10 new file mode 100644 index 0000000000..a6647a8223 --- /dev/null +++ b/winsup/cygwin/release/3.6.10 @@ -0,0 +1,7 @@ +Fixes: +------ + +- Fix deadlock in console teardown that arises from pseudo console. + Addresses: https://github.com/msys2/msys2-runtime/issues/338 + +- Fix handling of surrogate pair in legacy console. From 9dab20d03be12369a48c46ab1fb3ee76ba730203 Mon Sep 17 00:00:00 2001 From: Mark Geisert Date: Wed, 27 May 2026 22:42:44 -0700 Subject: [PATCH 006/102] Cygwin: Ensure unused fd available for open() The existing logic for open() assumes an fd is always available in the fdtable for a created file. This leads to a situation where, if there is no fd available due to the OPEN_MAX limit being hit, the file is created but cannot be referenced by a Cygwin fd. Move the fd reservation code to an earlier location within open(). Reported-by: Christian Franke Addresses: https://cygwin.com/pipermail/cygwin/2026-May/259664.html Signed-off-by: Mark Geisert Fixes: e859706578ba (* autoload.cc (NtCreateFile): Add.) (cherry picked from commit 31bf91f867c5fadd7deb408cf06fe3af8e86bb74) --- winsup/cygwin/syscalls.cc | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/winsup/cygwin/syscalls.cc b/winsup/cygwin/syscalls.cc index 7a8e5d4fd5..2bea797680 100644 --- a/winsup/cygwin/syscalls.cc +++ b/winsup/cygwin/syscalls.cc @@ -1547,6 +1547,13 @@ open (const char *unix_path, int flags, ...) fh = fh_file; } + /* Reserve an fdtable entry here, before calling open_with_arch() below. + Otherwise there's a tiny chance of hitting OPEN_MAX further on which + could create a new file without any way for Cygwin to refer to it. */ + cygheap_fdnew fd; + if (fd < 0) + __leave; /* errno already set */ + if (fh->dev () == FH_PROCESSFD && fh->pc.follow_fd_symlink ()) { /* Reopen file by descriptor */ @@ -1573,14 +1580,6 @@ open (const char *unix_path, int flags, ...) try_to_bin (fh->pc, fh->get_handle (), DELETE, FILE_OPEN_FOR_BACKUP_INTENT); - cygheap_fdnew fd; - - if (fd < 0) - { - fh->close(); - __leave; /* errno already set */ - } - fd = fh; if (fd <= 2) set_std_handle (fd); From 620ad4894bf820f6e772b1b9e10a253609191f72 Mon Sep 17 00:00:00 2001 From: Jon Turney Date: Wed, 10 Jun 2026 13:55:34 +0100 Subject: [PATCH 007/102] Cygwin: Add recent OPEN_MAX fix into release note Fixes: 31bf91f867c5 ("Cygwin: Ensure unused fd available for open()") Signed-off-by: Jon Turney (cherry picked from commit 35fcbb8cfbccc34d02611d54a06731dfcc075578) --- winsup/cygwin/release/3.6.10 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/winsup/cygwin/release/3.6.10 b/winsup/cygwin/release/3.6.10 index a6647a8223..e29c3d6c05 100644 --- a/winsup/cygwin/release/3.6.10 +++ b/winsup/cygwin/release/3.6.10 @@ -5,3 +5,6 @@ Fixes: Addresses: https://github.com/msys2/msys2-runtime/issues/338 - Fix handling of surrogate pair in legacy console. + +- Fix behaviour with OPEN_MAX files open + Addresses: https://cygwin.com/pipermail/cygwin/2026-May/259664.html From 3da6529358fa4d9c880b53309a81c12192fdaacd Mon Sep 17 00:00:00 2001 From: Mark Geisert Date: Mon, 8 Jun 2026 15:10:09 -0700 Subject: [PATCH 008/102] Cygwin: Fix chown commands in cygserver-config Change "chown 18.544" to "18:544" in two locations. Reported-by: Lionel Cons Addresses: Signed-off-by: Mark Geisert Fixes: b5a7cb02cd9d (* cygserver-config: Use numeric id 18 instead of "system" in chown.) (cherry picked from commit f32a05ec3d73bd0ff917d94d8bacaa035f56e773) --- winsup/cygserver/cygserver-config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winsup/cygserver/cygserver-config b/winsup/cygserver/cygserver-config index abda186449..3130de7bcd 100755 --- a/winsup/cygserver/cygserver-config +++ b/winsup/cygserver/cygserver-config @@ -162,7 +162,7 @@ then exit 1 fi chmod 664 "${SYSCONFDIR}/cygserver.conf" - chown 18.544 "${SYSCONFDIR}/cygserver.conf" + chown 18:544 "${SYSCONFDIR}/cygserver.conf" fi # On NT ask if cygserver should be installed as service @@ -194,7 +194,7 @@ then echo "To start it, call \`net start ${service_name}' or \`cygrunsrv -S ${service_name}'." fi touch "${LOCALSTATEDIR}/log/cygserver.log" - chown 18.544 "${LOCALSTATEDIR}/log/cygserver.log" + chown 18:544 "${LOCALSTATEDIR}/log/cygserver.log" fi fi From dcd3455c4ce6a28e7dfada6ee2342dfab8be6983 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Tue, 9 Jun 2026 09:14:57 +0900 Subject: [PATCH 009/102] Cygwin: clipboard: Add workaround for ERROR_CLIPBOARD_NOT_OPEN SetClipboardData() and GetClipboardData() occasionally fail with ERROR_CLIPBOARD_NOT_OPEN, even though OpenClipboard() succeeded if NULL HWND is used. Retry until GetClipboardData() does not return ERROR_CLIPBOARD_NOT_OPEN. Addresses: https://cygwin.com/pipermail/cygwin/2026-February/259438.html Signed-off-by: Takashi Yano Reviewed-by: Mark Geisert (cherry picked from commit 7fd670a36b9a26c357f61650a6ec4a0e09936b7c) --- winsup/cygwin/fhandler/clipboard.cc | 21 +++++++++++++++++++-- winsup/cygwin/release/3.6.10 | 3 +++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/winsup/cygwin/fhandler/clipboard.cc b/winsup/cygwin/fhandler/clipboard.cc index 12691c7c16..116feb5994 100644 --- a/winsup/cygwin/fhandler/clipboard.cc +++ b/winsup/cygwin/fhandler/clipboard.cc @@ -25,11 +25,28 @@ details. */ static inline bool open_clipboard () { - const int max_retry = 10; + const int max_retry = 20; for (int i = 0; i < max_retry; i++) { + /* No appropriate HWND exists here. */ if (OpenClipboard (NULL)) - return true; + { + /* SetClipboardData() and GetClipboardData() occasionally + fail with ERROR_CLIPBOARD_NOT_OPEN, even though + OpenClipboard() succeeded if NULL HWND is used. + Retry until GetClipboardData() does not return + ERROR_CLIPBOARD_NOT_OPEN. */ + if (GetClipboardData (CF_UNICODETEXT)) + return true; + DWORD err = GetLastError (); + /* Here, ERROR_NOT_FOUND means the clipboard does not contains + valid CF_UNICODETEXT. OpenClipboard() must have succeeded. */ + if (err == ERROR_NOT_FOUND) + return true; + CloseClipboard (); + if (err != ERROR_CLIPBOARD_NOT_OPEN) + return false; + } Sleep (1); } return false; diff --git a/winsup/cygwin/release/3.6.10 b/winsup/cygwin/release/3.6.10 index e29c3d6c05..506f79b215 100644 --- a/winsup/cygwin/release/3.6.10 +++ b/winsup/cygwin/release/3.6.10 @@ -8,3 +8,6 @@ Fixes: - Fix behaviour with OPEN_MAX files open Addresses: https://cygwin.com/pipermail/cygwin/2026-May/259664.html + +- Add workaround for clipboard access error. + Addresses: https://cygwin.com/pipermail/cygwin/2026-February/259438.html From cad596e35adeb1e56ba65c730b73ea9e6e0fe915 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Mon, 8 Jun 2026 20:54:44 +0900 Subject: [PATCH 010/102] Cygwin: pty: Do not set input_available_event when applying line_edit() The commit a0b38a81b9be sets input_available_event even if the transferred input is still in the readahead buffer and is not ready to read. The SetEvent() is called in accept_input() via line_edit(), so setting this event here is not correct. This causes the issue that read() returns 0 instead of blocking until accept_input() is called. This patch removes this SetEvent() call. Fixes: a0b38a81b9be ("Cygwin: pty: Apply line_edit() for transferred input to to_cyg") Addresses: https://cygwin.com/pipermail/cygwin/2026-June/259776.html Reported-by: Koichi Murase Signed-off-by: Takashi Yano Reviewed-by: Mark Geisert (cherry picked from commit f977d6edb4659a7ea1e341e2002f6d107e7d8013) --- winsup/cygwin/fhandler/pty.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index cae047a089..e5ffccede4 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -2746,7 +2746,6 @@ fhandler_pty_master::apply_line_edit_to_transferred_input () n -= ret; p += ret; } - SetEvent (input_available_event); } static DWORD From 4060cf35e7ec8d035b8fc63bc044996ed906364d Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Mon, 8 Jun 2026 22:03:20 +0900 Subject: [PATCH 011/102] Cygwin: pty: Introduce a helper function get_handle_from_process() The current pty code performs the sequence: OpenProcess() -> DuplicateHandle() in various places. This helper function encapsulates that sequence to improve readability and maintainability. Signed-off-by: Takashi Yano Reviewed-by: Mark Geisert (cherry picked from commit c76e474c8c99d1e9690822c371acbe6244c6be00) --- winsup/cygwin/fhandler/pty.cc | 66 +++++++++++++++++------------------ 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index e5ffccede4..f216477181 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -2030,6 +2030,23 @@ fhandler_pty_common::close (int flag) return 0; } +static inline HANDLE +get_handle_from_process (DWORD pid, HANDLE h, bool inh = false) +{ + HANDLE ret = NULL; + HANDLE owner = OpenProcess (PROCESS_DUP_HANDLE, FALSE, pid); + if (owner) + { + if (!DuplicateHandle (owner, h, GetCurrentProcess (), &ret, 0, inh, + DUPLICATE_SAME_ACCESS)) + termios_printf ("DuplicateHandle() %p from process %d (%E)", h, pid); + CloseHandle (owner); + } + else + termios_printf ("OpenProcess (%d) failed (%E).", pid); + return ret; +} + void fhandler_pty_common::resize_pseudo_console (struct winsize *ws) { @@ -2037,15 +2054,14 @@ fhandler_pty_common::resize_pseudo_console (struct winsize *ws) size.X = ws->ws_col; size.Y = ws->ws_row; HPCON_INTERNAL hpcon_local; - HANDLE pcon_owner = - OpenProcess (PROCESS_DUP_HANDLE, FALSE, get_ttyp ()->nat_pipe_owner_pid); - DuplicateHandle (pcon_owner, get_ttyp ()->h_pcon_write_pipe, - GetCurrentProcess (), &hpcon_local.hWritePipe, - 0, FALSE, DUPLICATE_SAME_ACCESS); + hpcon_local.hWritePipe = + get_handle_from_process (get_ttyp ()->nat_pipe_owner_pid, + get_ttyp ()->h_pcon_write_pipe); + if (hpcon_local.hWritePipe == NULL) + return; acquire_attach_mutex (mutex_timeout); ResizePseudoConsole ((HPCON) &hpcon_local, size); release_attach_mutex (); - CloseHandle (pcon_owner); CloseHandle (hpcon_local.hWritePipe); } @@ -2295,18 +2311,13 @@ fhandler_pty_master::write (const void *ptr, size_t len) { if (h_pcon_in_dupped) ForceCloseHandle (h_pcon_in_dupped); - h_pcon_in_dupped = NULL; - nat_pipe_owner_pid_dupped = 0; - HANDLE pcon_owner = OpenProcess (PROCESS_DUP_HANDLE, FALSE, - get_ttyp ()->nat_pipe_owner_pid); - if (pcon_owner) - { - DuplicateHandle (pcon_owner, get_ttyp ()->h_pcon_in, - GetCurrentProcess (), &h_pcon_in_dupped, - 0, FALSE, DUPLICATE_SAME_ACCESS); - nat_pipe_owner_pid_dupped = get_ttyp ()->nat_pipe_owner_pid; - CloseHandle (pcon_owner); - } + h_pcon_in_dupped = + get_handle_from_process (get_ttyp ()->nat_pipe_owner_pid, + get_ttyp ()->h_pcon_in); + if (h_pcon_in_dupped) + nat_pipe_owner_pid_dupped = get_ttyp ()->nat_pipe_owner_pid; + else + nat_pipe_owner_pid_dupped = 0; } else { @@ -4065,16 +4076,9 @@ fhandler_pty_slave::transfer_input (tty::xfer_dir dir, HANDLE from, tty *ttyp, to = ttyp->to_slave (); pinfo p (ttyp->master_pid); - HANDLE pty_owner = NULL; if (p) - pty_owner = OpenProcess (PROCESS_DUP_HANDLE, FALSE, p->dwProcessId); - if (pty_owner) - { - DuplicateHandle (pty_owner, to, GetCurrentProcess (), &to, - 0, TRUE, DUPLICATE_SAME_ACCESS); - CloseHandle (pty_owner); - } - else + to = get_handle_from_process (p->dwProcessId, to, true); + if (to == NULL) { char pipe[MAX_PATH]; __small_sprintf (pipe, @@ -4371,12 +4375,8 @@ fhandler_pty_slave::setpgid_aux (pid_t pid) if (get_ttyp ()->pcon_activated && get_ttyp ()->nat_pipe_owner_pid && !get_console_process_id (get_ttyp ()->nat_pipe_owner_pid, true)) { - HANDLE pcon_owner = OpenProcess (PROCESS_DUP_HANDLE, FALSE, - get_ttyp ()->nat_pipe_owner_pid); - DuplicateHandle (pcon_owner, get_ttyp ()->h_pcon_in, - GetCurrentProcess (), &from, - 0, TRUE, DUPLICATE_SAME_ACCESS); - CloseHandle (pcon_owner); + from = get_handle_from_process (get_ttyp ()->nat_pipe_owner_pid, + get_ttyp ()->h_pcon_in, true); DWORD target_pid = get_ttyp ()->nat_pipe_owner_pid; resume_pid = attach_console_temporarily (target_pid); attach_restore = true; From 1c5c82a215abcd44e5d82ee4128c468baf20821f Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Mon, 8 Jun 2026 22:21:19 +0900 Subject: [PATCH 012/102] Cygwin: pty: Prevent unintended conversion for cursor position report When the cursor position report ("CSI m;n R") is transferred from cyg-pipe to nat-pipe, it is undesirably converted into Fn3 key by pseudo console. This patch adds a workaround to prevent this unintended conversion for cursor position report by enabling ENABLE_VIRTUAL_TERMINAL_INPUT flag temporarily. Addresses: https://cygwin.com/pipermail/cygwin/2026-June/259776.html Reported-by: Koichi Murase Signed-off-by: Takashi Yano Reviewed-by: Mark Geisert (cherry picked from commit ad65bc9742f2c232a2b8744f69fc31923965d768) --- winsup/cygwin/fhandler/pty.cc | 53 +++++++++++++++++++++++++++++- winsup/cygwin/local_includes/tty.h | 1 + 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index f216477181..c3895aac8a 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -2251,7 +2251,6 @@ fhandler_pty_master::write (const void *ptr, size_t len) ixput = 0; state = 0; wp_tid = 0; - get_ttyp ()->req_xfer_input = false; get_ttyp ()->pcon_start = false; break; } @@ -2265,6 +2264,20 @@ fhandler_pty_master::write (const void *ptr, size_t len) && pp && pp->pgid == get_ttyp ()->getpgid () && get_ttyp ()->pty_input_state_eq (tty::to_cyg)) { + if (!get_ttyp ()->req_xfer_input) + { + HANDLE pcon_handle_ready_event = + get_ttyp ()->pcon_handle_ready_event; + get_handle_from_process (get_ttyp ()->nat_pipe_owner_pid, + pcon_handle_ready_event); + if (pcon_handle_ready_event) + { + cygwait (pcon_handle_ready_event, INFINITE); + ResetEvent (pcon_handle_ready_event); + CloseHandle (pcon_handle_ready_event); + } + } + /* This accept_input() call is needed in order to transfer input which is not accepted yet to non-cygwin pipe. */ WaitForSingleObject (input_mutex, mutex_timeout); @@ -2278,6 +2291,7 @@ fhandler_pty_master::write (const void *ptr, size_t len) release_attach_mutex (); ReleaseMutex (input_mutex); } + get_ttyp ()->req_xfer_input = false; get_ttyp ()->pcon_start_pid = 0; } if (len == 0) @@ -3567,6 +3581,8 @@ fhandler_pty_slave::setup_pseudoconsole () si.StartupInfo.hStdOutput = NULL; si.StartupInfo.hStdError = NULL; + get_ttyp ()->pcon_handle_ready_event = + CreateEvent (&sec_none_nih, TRUE, FALSE, NULL); get_ttyp ()->pcon_activated = true; get_ttyp ()->pcon_start = true; get_ttyp ()->pcon_start_pid = myself->pid; @@ -3653,6 +3669,7 @@ fhandler_pty_slave::setup_pseudoconsole () /* Discard the pseudo console handler container here. Reconstruct it temporary when it is needed. */ HeapFree (GetProcessHeap (), 0, hp); + SetEvent (get_ttyp ()->pcon_handle_ready_event); } acquire_attach_mutex (mutex_timeout); @@ -3860,6 +3877,11 @@ fhandler_pty_slave::close_pseudoconsole (tty *ttyp, DWORD force_switch_to) ttyp->pcon_start = false; ttyp->pcon_start_pid = 0; } + if (ttyp->pcon_handle_ready_event) + { + CloseHandle (ttyp->pcon_handle_ready_event); + ttyp->pcon_handle_ready_event = NULL; + } } else { /* Just detach from the pseudo console if I am not owner. */ @@ -4108,6 +4130,26 @@ fhandler_pty_slave::transfer_input (tty::xfer_dir dir, HANDLE from, tty *ttyp, UINT cp_from = 0, cp_to = 0; + HANDLE h_pcon_in = NULL; + DWORD con_mode = 0; + if (ttyp->pcon_activated && dir == tty::to_nat) + { + /* Escape sequences such as the cursor position report ("CSI m;n R") + are undesirably converted into an Fn3 key by pseudo console. + To privent this unintended conversion, temporarily enable + ENABLE_VIRTUAL_TERMINAL_INPUT flag. */ + h_pcon_in = + get_handle_from_process (ttyp->nat_pipe_owner_pid, ttyp->h_pcon_in); + if (h_pcon_in) + { + DWORD target_pid = ttyp->nat_pipe_owner_pid; + DWORD resume_pid = attach_console_temporarily (target_pid); + GetConsoleMode (h_pcon_in, &con_mode); + SetConsoleMode (h_pcon_in, con_mode | ENABLE_VIRTUAL_TERMINAL_INPUT); + resume_from_temporarily_attach (resume_pid); + } + } + if (dir == tty::to_nat) { cp_from = ttyp->term_code_page; @@ -4222,6 +4264,15 @@ fhandler_pty_slave::transfer_input (tty::xfer_dir dir, HANDLE from, tty *ttyp, } CloseHandle (to); + if (h_pcon_in) + { + DWORD target_pid = ttyp->nat_pipe_owner_pid; + DWORD resume_pid = attach_console_temporarily (target_pid); + SetConsoleMode (h_pcon_in, con_mode); + resume_from_temporarily_attach (resume_pid); + CloseHandle (h_pcon_in); + } + ttyp->pty_input_state = dir; /* Fix input_available_event which indicates availability in cyg pipe. */ if (dir == tty::to_nat) /* all data is transfered to nat pipe, diff --git a/winsup/cygwin/local_includes/tty.h b/winsup/cygwin/local_includes/tty.h index 6e70a74cd7..a03e965e4d 100644 --- a/winsup/cygwin/local_includes/tty.h +++ b/winsup/cygwin/local_includes/tty.h @@ -120,6 +120,7 @@ class tty: public tty_min pid_t pcon_start_pid; bool switch_to_nat_pipe; DWORD nat_pipe_owner_pid; + HANDLE pcon_handle_ready_event; UINT term_code_page; ULONGLONG fwd_last_time; bool fwd_not_empty; From 03ab852f07ab64b8a709d23711a52eadc24d1c62 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Tue, 23 Jun 2026 09:57:28 +0900 Subject: [PATCH 013/102] Add fix for CPR response to release note (cherry picked from commit d21aaac37996a5c49144c1fed0116eae1f302d86) --- winsup/cygwin/release/3.6.10 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/winsup/cygwin/release/3.6.10 b/winsup/cygwin/release/3.6.10 index 506f79b215..4820731920 100644 --- a/winsup/cygwin/release/3.6.10 +++ b/winsup/cygwin/release/3.6.10 @@ -11,3 +11,6 @@ Fixes: - Add workaround for clipboard access error. Addresses: https://cygwin.com/pipermail/cygwin/2026-February/259438.html + +- Fix broken cursor position report resonse when a non-cygwin app starts. + Addresses: https://cygwin.com/pipermail/cygwin/2026-June/259776.html From a828b4ef1753b3d7b6a581c1c4239dd7c9d320d5 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Thu, 11 Jun 2026 00:41:34 +0900 Subject: [PATCH 014/102] Cygwin: pty: Fix race issue between starting and exiting non-cygwin apps Without this patch, when a non-cygwin program (A) is about to exit, and another non-cygwin program (B) is started, input transferring between cyg-pipe and nat-pipe may not work as expected. When the non-cygwin program (A) exits, input transferring from nat-pipe to cyg-pipe will be performed. However, the the non-cygwin program (B) will performs input transferring from cyg-pipe to nat-pipe at the same time. The mechanism of the problem is as follows. 1) The the non-cygwin program (A) checks current input pipe state, then it is nat-pipe since the this program is a non-cygwin program. The program (A) also checks if any handover target exists, but it is not found since the program (B) is not started yet. So, the program (A) decided to transfer input form nat-pipe to cyg- pipe. 2) Before the non-cygwin (A) program performs input transferring, if the non-cygwin program (B) is started and checks the input pipe state, it is nat-pipe state, so the non-cygwin program (B) does not perform input transferring. 3) However, just after that, the non-cygwin program (A) performs input transferring from nat-pipe to cyg-pipe, so typeahead input will be stored in cyg-pipe. 4) The non-cygwin program (B) cannot read the typeahead input because it is now in the cyg-pipe. The following code demonstrates the issue. #include #include #include int main(int argc, char *argv[]) { int n = 1; if (argc > 1) n = atoi(argv[1]); if (fork()) { execlp("cmd.exe", "cmd", NULL); perror("execlp(\"cmd\"): "); } for (int i=0; i Reviewed-by: Mark Geisert (cherry picked from commit f3eecb723bed090b73753b3d428329e90c960aac) --- winsup/cygwin/fhandler/pty.cc | 98 ++++++++++++++----------- winsup/cygwin/local_includes/fhandler.h | 2 + winsup/cygwin/release/3.6.10 | 2 + 3 files changed, 61 insertions(+), 41 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index c3895aac8a..b1e42dafb9 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -492,8 +492,7 @@ fhandler_pty_master::accept_input () HANDLE write_to = get_output_handle (); tmp_pathbuf tp; - if (to_be_read_from_nat_pipe () - && get_ttyp ()->pty_input_state == tty::to_nat) + if (get_ttyp ()->pty_input_state == tty::to_nat) { /* This code is reached if non-cygwin app is foreground and pseudo console is not enabled. */ @@ -1109,18 +1108,18 @@ fhandler_pty_slave::reset_switch_to_nat_pipe (void) mutex_timeout = INFINITE; if (isHybrid) { + WaitForSingleObject (input_mutex, mutex_timeout); if (get_ttyp ()->getpgid () == myself->pgid && GetStdHandle (STD_INPUT_HANDLE) == get_handle () && get_ttyp ()->pty_input_state_eq (tty::to_nat)) { - WaitForSingleObject (input_mutex, mutex_timeout); acquire_attach_mutex (mutex_timeout); transfer_input (tty::to_cyg, get_handle_nat (), get_ttyp (), input_available_event, input_transferred_to_cyg); release_attach_mutex (); - ReleaseMutex (input_mutex); } + ReleaseMutex (input_mutex); if (get_ttyp ()->master_is_running_as_service && get_ttyp ()->pcon_activated) /* If the master is running as service, re-attaching to @@ -2181,6 +2180,22 @@ fhandler_pty_master::close (int flag) return 0; } +line_edit_status +fhandler_pty_master::line_edit_maybe (const char *ptr, size_t len, + termios &ti, ssize_t *n) +{ + DWORD m; + if (get_ttyp ()->req_xfer_input + && get_ttyp ()->pty_input_state_eq (tty::to_nat)) + { + WriteFile (to_slave_nat, ptr, len, &m, NULL); + *n = (ssize_t) m; + return line_edit_ok; + } + else + return line_edit (ptr, len, ti, n); +} + ssize_t fhandler_pty_master::write (const void *ptr, size_t len) { @@ -2198,6 +2213,25 @@ fhandler_pty_master::write (const void *ptr, size_t len) get_ttyp ()->discard_input = false; + /* This input transfer is needed when cygwin-app which is started from + non-cygwin app is terminated while pseudo console is disabled. */ + if (!get_ttyp ()->pcon_activated && !get_ttyp ()->pcon_start + && to_be_read_from_nat_pipe ()) + { + WaitForSingleObject (input_mutex, mutex_timeout); + if (get_ttyp ()->nat_fg (get_ttyp ()->getpgid ()) + && get_ttyp ()->pty_input_state == tty::to_cyg) + { + acquire_attach_mutex (mutex_timeout); + fhandler_pty_slave::transfer_input (tty::to_nat, from_master, + get_ttyp (), + input_available_event, + input_transferred_to_cyg); + release_attach_mutex (); + } + ReleaseMutex (input_mutex); + } + if (get_ttyp ()->pcon_start) { /* Reaches here when pseudo console initialization is on going. */ /* Pseudo condole support uses "CSI6n" to get cursor position. @@ -2218,7 +2252,7 @@ fhandler_pty_master::write (const void *ptr, size_t len) if (p[i] == '\033') { if (ixput) - line_edit (wpbuf, ixput, ti, &ret); + line_edit_maybe (wpbuf, ixput, ti, &ret); ixput = 0; state = 1; wp_tid = _my_tls.thread_id; @@ -2236,7 +2270,7 @@ fhandler_pty_master::write (const void *ptr, size_t len) } } else - line_edit (p + i, 1, ti, &ret); + line_edit_maybe (p + i, 1, ti, &ret); len = orig_len - i - 1; ptr = p + i + 1; if (state == 1 && wp_tid == _my_tls.thread_id && p[i] == 'R') @@ -2259,6 +2293,7 @@ fhandler_pty_master::write (const void *ptr, size_t len) if (!get_ttyp ()->pcon_start) { /* Pseudo console initialization has been done in above code. */ + WaitForSingleObject (input_mutex, mutex_timeout); pinfo pp (get_ttyp ()->pcon_start_pid); if (get_ttyp ()->switch_to_nat_pipe && pp && pp->pgid == get_ttyp ()->getpgid () @@ -2268,8 +2303,9 @@ fhandler_pty_master::write (const void *ptr, size_t len) { HANDLE pcon_handle_ready_event = get_ttyp ()->pcon_handle_ready_event; - get_handle_from_process (get_ttyp ()->nat_pipe_owner_pid, - pcon_handle_ready_event); + pcon_handle_ready_event = + get_handle_from_process (get_ttyp ()->nat_pipe_owner_pid, + pcon_handle_ready_event); if (pcon_handle_ready_event) { cygwait (pcon_handle_ready_event, INFINITE); @@ -2280,7 +2316,6 @@ fhandler_pty_master::write (const void *ptr, size_t len) /* This accept_input() call is needed in order to transfer input which is not accepted yet to non-cygwin pipe. */ - WaitForSingleObject (input_mutex, mutex_timeout); if (get_readahead_valid ()) accept_input (); acquire_attach_mutex (mutex_timeout); @@ -2289,9 +2324,9 @@ fhandler_pty_master::write (const void *ptr, size_t len) input_available_event, input_transferred_to_cyg); release_attach_mutex (); - ReleaseMutex (input_mutex); } get_ttyp ()->req_xfer_input = false; + ReleaseMutex (input_mutex); get_ttyp ()->pcon_start_pid = 0; } if (len == 0) @@ -2301,7 +2336,7 @@ fhandler_pty_master::write (const void *ptr, size_t len) /* Write terminal input to to_slave_nat pipe instead of output_handle if current application is native console application. */ WaitForSingleObject (input_mutex, mutex_timeout); - if (to_be_read_from_nat_pipe () && get_ttyp ()->pcon_activated + if (get_ttyp ()->pcon_activated && get_ttyp ()->pty_input_state == tty::to_nat) { /* Reaches here when non-cygwin app is foreground and pseudo console is activated. */ @@ -2385,20 +2420,6 @@ fhandler_pty_master::write (const void *ptr, size_t len) /* The code path reaches here when pseudo console is not activated or cygwin process is foreground even though pseudo console is activated. */ - - /* This input transfer is needed when cygwin-app which is started from - non-cygwin app is terminated if pseudo console is disabled. */ - if (to_be_read_from_nat_pipe () && !get_ttyp ()->pcon_activated - && get_ttyp ()->nat_fg (get_ttyp ()->getpgid ()) - && get_ttyp ()->pty_input_state == tty::to_cyg) - { - acquire_attach_mutex (mutex_timeout); - fhandler_pty_slave::transfer_input (tty::to_nat, from_master, - get_ttyp (), input_available_event, - input_transferred_to_cyg); - release_attach_mutex (); - } - line_edit_status status = line_edit (p, len, ti, &ret); ReleaseMutex (input_mutex); @@ -4337,9 +4358,9 @@ fhandler_pty_slave::setup_for_non_cygwin_app (bool nopcon, const WCHAR *envblock, bool stdin_is_ptys) { + WaitForSingleObject (pipe_sw_mutex, INFINITE); if (disable_pcon || !term_has_pcon_cap (envblock)) nopcon = true; - WaitForSingleObject (pipe_sw_mutex, INFINITE); /* Setting switch_to_nat_pipe is necessary even if pseudo console will not be activated. */ fhandler_base *fh = ::cygheap->fdtab[0]; @@ -4355,16 +4376,16 @@ fhandler_pty_slave::setup_for_non_cygwin_app (bool nopcon, pcon_enabled = setup_pseudoconsole (); ReleaseMutex (pipe_sw_mutex); /* For pcon enabled case, transfer_input() is called in master::write() */ + WaitForSingleObject (input_mutex, mutex_timeout); if (!pcon_enabled && get_ttyp ()->getpgid () == myself->pgid && stdin_is_ptys && get_ttyp ()->pty_input_state_eq (tty::to_cyg)) { - WaitForSingleObject (input_mutex, mutex_timeout); acquire_attach_mutex (mutex_timeout); transfer_input (tty::to_nat, get_handle (), get_ttyp (), input_available_event, input_transferred_to_cyg); release_attach_mutex (); - ReleaseMutex (input_mutex); } + ReleaseMutex (input_mutex); } void @@ -4373,22 +4394,22 @@ fhandler_pty_slave::cleanup_for_non_cygwin_app (handle_set_t *p, tty *ttyp, DWORD force_switch_to) { ttyp->wait_fwd (); + WaitForSingleObject (p->pipe_sw_mutex, INFINITE); + WaitForSingleObject (p->input_mutex, mutex_timeout); if (nat_pipe_owner_self (ttyp->nat_pipe_owner_pid)) { DWORD switch_to = get_winpid_to_hand_over (ttyp, force_switch_to); if ((!switch_to && (ttyp->pcon_activated || stdin_is_ptys)) && ttyp->pty_input_state_eq (tty::to_nat)) { - WaitForSingleObject (p->input_mutex, mutex_timeout); acquire_attach_mutex (mutex_timeout); transfer_input (tty::to_cyg, p->from_master_nat, ttyp, p->input_available_event, p->input_transferred_to_cyg); release_attach_mutex (); - ReleaseMutex (p->input_mutex); } } - WaitForSingleObject (p->pipe_sw_mutex, INFINITE); + ReleaseMutex (p->input_mutex); if (ttyp->pcon_activated) close_pseudoconsole (ttyp, force_switch_to); else @@ -4402,27 +4423,23 @@ fhandler_pty_slave::setpgid_aux (pid_t pid) reset_switch_to_nat_pipe (); WaitForSingleObject (pipe_sw_mutex, INFINITE); + WaitForSingleObject (input_mutex, mutex_timeout); bool was_nat_fg = get_ttyp ()->nat_fg (tc ()->pgid); bool nat_fg = get_ttyp ()->nat_fg (pid); if (!was_nat_fg && nat_fg && get_ttyp ()->switch_to_nat_pipe && get_ttyp ()->pty_input_state_eq (tty::to_cyg)) { - ReleaseMutex (pipe_sw_mutex); - WaitForSingleObject (input_mutex, mutex_timeout); acquire_attach_mutex (mutex_timeout); transfer_input (tty::to_nat, get_handle (), get_ttyp (), input_available_event, input_transferred_to_cyg); release_attach_mutex (); - ReleaseMutex (input_mutex); } else if (was_nat_fg && !nat_fg && get_ttyp ()->switch_to_nat_pipe && get_ttyp ()->pty_input_state_eq (tty::to_nat)) { - ReleaseMutex (pipe_sw_mutex); bool attach_restore = false; HANDLE from = get_handle_nat (); DWORD resume_pid = 0; - WaitForSingleObject (input_mutex, mutex_timeout); if (get_ttyp ()->pcon_activated && get_ttyp ()->nat_pipe_owner_pid && !get_console_process_id (get_ttyp ()->nat_pipe_owner_pid, true)) { @@ -4440,10 +4457,9 @@ fhandler_pty_slave::setpgid_aux (pid_t pid) resume_from_temporarily_attach (resume_pid); else release_attach_mutex (); - ReleaseMutex (input_mutex); } - else - ReleaseMutex (pipe_sw_mutex); + ReleaseMutex (input_mutex); + ReleaseMutex (pipe_sw_mutex); } bool @@ -4453,8 +4469,8 @@ fhandler_pty_master::need_send_ctrl_c_event () apps will be done in pseudo console, therefore, sending it in fhandler_pty_master::write() duplicates that event for non-cygwin apps. So return false if pseudo console is activated. */ - return !(to_be_read_from_nat_pipe () && get_ttyp ()->pcon_activated - && get_ttyp ()->pty_input_state == tty::to_nat); + return !(get_ttyp ()->pcon_activated + && get_ttyp ()->pty_input_state == tty::to_nat); } void diff --git a/winsup/cygwin/local_includes/fhandler.h b/winsup/cygwin/local_includes/fhandler.h index 09e04c14f4..3a4bfac170 100644 --- a/winsup/cygwin/local_includes/fhandler.h +++ b/winsup/cygwin/local_includes/fhandler.h @@ -2632,6 +2632,8 @@ class fhandler_pty_master: public fhandler_pty_common void get_master_fwd_thread_param (master_fwd_thread_param_t *p); bool need_send_ctrl_c_event (); void apply_line_edit_to_transferred_input (); + line_edit_status line_edit_maybe (const char *p, size_t len, termios&, + ssize_t *n); }; class fhandler_dev_null: public fhandler_base diff --git a/winsup/cygwin/release/3.6.10 b/winsup/cygwin/release/3.6.10 index 4820731920..e37ccb3919 100644 --- a/winsup/cygwin/release/3.6.10 +++ b/winsup/cygwin/release/3.6.10 @@ -14,3 +14,5 @@ Fixes: - Fix broken cursor position report resonse when a non-cygwin app starts. Addresses: https://cygwin.com/pipermail/cygwin/2026-June/259776.html + +- Fix race issue between starting and exiting non-cygwin apps in pty. From f2faf906732ba2cbcaa00199e8cd12b189e04c79 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Fri, 12 Jun 2026 21:12:20 +0900 Subject: [PATCH 015/102] Cygwin: pty: Treat CR/NL in accept_input() the same as in transfer_input() In transfer_input(), CR and NL in the data transferred to nat-pipe is treated as follows: 1) If pseudo console is activated, convert NL to CR. 2) If pseudo console is disabled, convert CR to NL. This conversion is necessary to ensure non-cygwin apps can handle CR/NL as expected. Therefor, CR and NL should be treated as the same way in accept_input() if the data is sent to nat-pipe. In the previous implementation, problems rarely occurred because accept_input() normally does not handle input for non-cygwin apps when the pseudo console is active. Under typical conditions, such input is set to pseudo console directly by WriteFile(), so accept_input() is not involved and no conversion issues arise. There is, however, a brief period during pseudo console initialization in which accept_input *does* handle the input. This happens because master::write() invokes line_edit() while in pcons_start mode. During this short window, the input is processed in pseudo-console-enabled mode, and the usual conversion behaviour may not apply. When the pseudo console is disabled, accept_input() always handles the input, and in most cases the ICRNL flag is set by shell, so line_edit() performs the CR->NL conversion. But if the flag is not set, this conversion does not occur. Therefore, the additional conversion introduced by this patch is required to ensure consistent behaviour in both cases. Fixes: f20641789427 ("Cygwin: pty: Reduce unecessary input transfer.") Signed-off-by: Takashi Yano Reviewed-by: Mark Geisert (cherry picked from commit 0f6dad639e1791d95cffd7a2490b82dff354eec2) --- winsup/cygwin/fhandler/pty.cc | 8 ++++++++ winsup/cygwin/release/3.6.10 | 2 ++ 2 files changed, 10 insertions(+) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index b1e42dafb9..d27ba4e59d 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -525,6 +525,14 @@ fhandler_pty_master::accept_input () p = mbbuf; bytes_left = nlen; } + + char *p0 = p; + if (get_ttyp ()->pcon_activated) + while ((p0 = (char *) memchr (p0, '\n', bytes_left - (p0 - p)))) + *p0 = '\r'; + else + while ((p0 = (char *) memchr (p0, '\r', bytes_left - (p0 - p)))) + *p0 = '\n'; } if (!bytes_left) diff --git a/winsup/cygwin/release/3.6.10 b/winsup/cygwin/release/3.6.10 index e37ccb3919..4d847829ba 100644 --- a/winsup/cygwin/release/3.6.10 +++ b/winsup/cygwin/release/3.6.10 @@ -16,3 +16,5 @@ Fixes: Addresses: https://cygwin.com/pipermail/cygwin/2026-June/259776.html - Fix race issue between starting and exiting non-cygwin apps in pty. + +- Fix CR/NL conversion in accept_input() for pty. From 4fb90734eda2cf219f3bc81eb13efd0d385a8aef Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Wed, 10 Jun 2026 23:05:29 +0900 Subject: [PATCH 016/102] Cygwin: console: Ensure the master thread runs only when it is supposed to Currently, disabling cons_master_thread is done by just setting the flag disable_master_thread. In fact, actual suspension of master thread is delayed a bit. Therefore, non-cygwin program where the master thread should be disabled may run even though the master thread is running in a short time. This patch ensure that the master thread is suspended when non-cygwin app is running. In addition, while master thread is running, console mode should not be changed. Therefore, the order of set_input_mode() call and disabling/enabling master thread is swapped. Fixes: d2b14c303c04 ("Cygwin: console: Redesign handling of special keys.") Signed-off-by: Takashi Yano Reviewed-by: Mark Geisert (cherry picked from commit 733d5a953fa952ba9572ef00019f3a6c70a2da73) --- winsup/cygwin/fhandler/console.cc | 15 +++++++++------ winsup/cygwin/release/3.6.10 | 2 ++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/winsup/cygwin/fhandler/console.cc b/winsup/cygwin/fhandler/console.cc index ca32d7faa7..e279556295 100644 --- a/winsup/cygwin/fhandler/console.cc +++ b/winsup/cygwin/fhandler/console.cc @@ -420,6 +420,7 @@ fhandler_console::cons_master_thread (handle_set_t *p, tty *ttyp) if (con.disable_master_thread) { + con.master_thread_suspended = true; cygwait (40); continue; } @@ -934,9 +935,9 @@ fhandler_console::setup_for_non_cygwin_app () console mode. */ if (get_ttyp ()->getpgid () == myself->pgid) { + set_disable_master_thread (true, this); set_input_mode (tty::native, &tc ()->ti, get_handle_set ()); set_output_mode (tty::native, &tc ()->ti, get_handle_set ()); - set_disable_master_thread (true, this); } } @@ -948,12 +949,12 @@ fhandler_console::cleanup_for_non_cygwin_app (handle_set_t *p) termios *ti = shared_console_info[unit] ? &(shared_console_info[unit]->tty_min_state.ti) : &dummy; /* Cleaning-up console mode for non-cygwin app. */ - set_disable_master_thread (con.owner == GetCurrentProcessId ()); /* conmode can be tty::restore when non-cygwin app is exec'ed from login shell. */ tty::cons_mode conmode = cons_mode_on_close (p); set_output_mode (conmode, ti, p); set_input_mode (conmode, ti, p); + set_disable_master_thread (con.owner == GetCurrentProcessId ()); } /* Return the tty structure associated with a given tty number. If the @@ -1146,8 +1147,8 @@ fhandler_console::bg_check (int sig, bool dontsignal) in the same process group. */ if (sig == SIGTTIN && con.curr_input_mode != tty::cygwin) { - set_disable_master_thread (false, this); set_input_mode (tty::cygwin, &tc ()->ti, get_handle_set ()); + set_disable_master_thread (false, this); } if (sig == SIGTTOU && con.curr_output_mode != tty::cygwin) set_output_mode (tty::cygwin, &tc ()->ti, get_handle_set ()); @@ -2023,8 +2024,8 @@ fhandler_console::post_open_setup (int fd) /* Setting-up console mode for cygwin app started from non-cygwin app. */ if (fd == 0) { - set_disable_master_thread (false, this); set_input_mode (tty::cygwin, &get_ttyp ()->ti, &handle_set); + set_disable_master_thread (false, this); } else if (fd == 1 || fd == 2) set_output_mode (tty::cygwin, &get_ttyp ()->ti, &handle_set); @@ -2043,9 +2044,9 @@ fhandler_console::close (int flag) && (dev_t) myself->ctty == get_device () && cons_mode_on_close (&handle_set) == tty::restore) { + set_disable_master_thread (true, this); set_output_mode (tty::restore, &get_ttyp ()->ti, &handle_set); set_input_mode (tty::restore, &get_ttyp ()->ti, &handle_set); - set_disable_master_thread (true, this); } if (shared_console_info[unit] && con.owner == GetCurrentProcessId ()) @@ -4369,10 +4370,10 @@ fhandler_console::set_console_mode_to_native () fhandler_console *cons = (fhandler_console *) (fhandler_base *) cfd; if (cons->get_device () == cons->tc ()->getntty ()) { + set_disable_master_thread (true, cons); termios *cons_ti = &cons->tc ()->ti; set_input_mode (tty::native, cons_ti, cons->get_handle_set ()); set_output_mode (tty::native, cons_ti, cons->get_handle_set ()); - set_disable_master_thread (true, cons); break; } } @@ -4734,6 +4735,8 @@ fhandler_console::set_disable_master_thread (bool x, fhandler_console *cons) cons->acquire_input_mutex (mutex_timeout); con.disable_master_thread = x; cons->release_input_mutex (); + while (con.master_thread_suspended != x) + Sleep (1); } int diff --git a/winsup/cygwin/release/3.6.10 b/winsup/cygwin/release/3.6.10 index 4d847829ba..597cf7bdbf 100644 --- a/winsup/cygwin/release/3.6.10 +++ b/winsup/cygwin/release/3.6.10 @@ -18,3 +18,5 @@ Fixes: - Fix race issue between starting and exiting non-cygwin apps in pty. - Fix CR/NL conversion in accept_input() for pty. + +- Ensure the cons_master_thread runs only when it is really supposed to. From 56dfa4db988c89ce9d216541e683cf65a294d8eb Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Wed, 10 Jun 2026 23:42:27 +0900 Subject: [PATCH 017/102] Cygwin: console: Fix NOFLSH behaviour a bit If you run "stty noflsh; cat" in "bash", and stop "cat" by Ctrl-C, a stray ^C is passed to "bash". The current code calls tcflush() if NOFLSH is not set, however, tcflush() is not called when NOFLSH is set. So, Ctrl-C remains in console input buffer. This should be discarded even in NOFLSH mode. This patch introduces a helper function discard_key_events() and call it to erase Ctrl-C in the console input buffer. Note that even with this patch, NOFLSH is not fully functional in console because the readahead buffer is unique to process, so it cannot be inherited to other processes. However, it should work intra process. Fixes: 118e51be1d04 ("(tty_min::kill_pgrp): Handle tty flush when signal detected.") Signed-off-by: Takashi Yano Reviewed-by: Mark Geisert (cherry picked from commit 66324edf64a9ef0672e445c870b5a38c091f7b38) --- winsup/cygwin/fhandler/console.cc | 20 +++++++++++++++++--- winsup/cygwin/fhandler/termios.cc | 10 +++++++--- winsup/cygwin/local_includes/fhandler.h | 2 ++ winsup/cygwin/release/3.6.10 | 2 ++ 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/winsup/cygwin/fhandler/console.cc b/winsup/cygwin/fhandler/console.cc index e279556295..acdccac601 100644 --- a/winsup/cygwin/fhandler/console.cc +++ b/winsup/cygwin/fhandler/console.cc @@ -1691,17 +1691,31 @@ fhandler_console::process_input_message (void) discard_len = 0; if (discard_len) { - DWORD discarded; acquire_attach_mutex (mutex_timeout); DWORD resume_pid = attach_console (con.owner); - ReadConsoleInputW (get_handle (), input_rec, discard_len, &discarded); + discard_key_events (discard_len); detach_console (resume_pid, con.owner); release_attach_mutex (); - con.num_processed -= min (con.num_processed, discarded); } return stat; } +void +fhandler_console::discard_key_events (size_t n) +{ + DWORD discarded = 0; + INPUT_RECORD input_rec[INREC_SIZE]; + DWORD n1 = min (INREC_SIZE, n); + while (n) + { + ReadConsoleInputW (get_handle (), input_rec, n1, &n1); + n -= n1; + discarded += n1; + n1 = min (INREC_SIZE, n); + } + con.num_processed -= min (con.num_processed, discarded); +} + bool dev_console::fillin (HANDLE h) { diff --git a/winsup/cygwin/fhandler/termios.cc b/winsup/cygwin/fhandler/termios.cc index e2822c3ee9..6508078505 100644 --- a/winsup/cygwin/fhandler/termios.cc +++ b/winsup/cygwin/fhandler/termios.cc @@ -666,9 +666,13 @@ fhandler_termios::sigflush () be NULL while this is alive. However, we can conceivably close a ctty while exiting and that will zero this. */ if ((!have_execed || have_execed_cygwin) && tc () - && (tc ()->getpgid () == myself->pgid) - && !(tc ()->ti.c_lflag & NOFLSH)) - tcflush (TCIFLUSH); + && (tc ()->getpgid () == myself->pgid)) + { + if (!(tc ()->ti.c_lflag & NOFLSH)) + tcflush (TCIFLUSH); + else + discard_key_events (1); + } } pid_t diff --git a/winsup/cygwin/local_includes/fhandler.h b/winsup/cygwin/local_includes/fhandler.h index 3a4bfac170..1abc2ece84 100644 --- a/winsup/cygwin/local_includes/fhandler.h +++ b/winsup/cygwin/local_includes/fhandler.h @@ -1982,6 +1982,7 @@ class fhandler_termios: public fhandler_base virtual off_t lseek (off_t, int); pid_t tcgetsid (); virtual int fstat (struct stat *buf); + virtual void discard_key_events (size_t n) {} fhandler_termios (void *) {} @@ -2360,6 +2361,7 @@ class fhandler_console: public fhandler_termios void wpbuf_put (char c); void wpbuf_send (); int fstat (struct stat *buf); + void discard_key_events (size_t n); class console_unit { diff --git a/winsup/cygwin/release/3.6.10 b/winsup/cygwin/release/3.6.10 index 597cf7bdbf..c583e7746c 100644 --- a/winsup/cygwin/release/3.6.10 +++ b/winsup/cygwin/release/3.6.10 @@ -20,3 +20,5 @@ Fixes: - Fix CR/NL conversion in accept_input() for pty. - Ensure the cons_master_thread runs only when it is really supposed to. + +- Fix NOFLSH behaviour in console a bit. From c92d8015863d71c5210f3793df7b8f1af0d2e88d Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Thu, 11 Jun 2026 00:00:34 +0900 Subject: [PATCH 018/102] Cygwin: console: Fix typeahead input for bash Currently, following misbehaviour occurs in bash. 1) Run "sleep 10". 2) Type "cmdps" while "sleep is running". 3) After "sleep" ends, "ps" does not run in "cmd". 4) exit from "cmd". Then, "ps" is executed. This is because process_input_message() handles all the events in the console input buffer, and stores key input into readahead buffer. However, since the readahead buffer is unique to process, "cmd" cannot read it. Since "ps" is stored in bash's readahead buffer, it is executed by bash after "cmd" exits. With this patch, process_input_message() handles only the requested amount of events by read(). Fixes: 8382778cdb57 ("Cygwin: console: fix select() behaviour") Signed-off-by: Takashi Yano Reviewed-by: Mark Geisert (cherry picked from commit fac73911f5a0732922168df216dc84cc730fe144) --- winsup/cygwin/fhandler/console.cc | 15 ++++++++++++--- winsup/cygwin/local_includes/fhandler.h | 2 +- winsup/cygwin/release/3.6.10 | 2 ++ winsup/cygwin/select.cc | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/winsup/cygwin/fhandler/console.cc b/winsup/cygwin/fhandler/console.cc index acdccac601..c7c3839a56 100644 --- a/winsup/cygwin/fhandler/console.cc +++ b/winsup/cygwin/fhandler/console.cc @@ -1220,7 +1220,7 @@ fhandler_console::read (void *pv, size_t& buflen) int ret; acquire_input_mutex (mutex_timeout); - ret = process_input_message (); + ret = process_input_message (buflen); switch (ret) { case input_error: @@ -1275,9 +1275,10 @@ fhandler_console::read (void *pv, size_t& buflen) } fhandler_console::input_states -fhandler_console::process_input_message (void) +fhandler_console::process_input_message (size_t len) { char tmp[60]; + size_t num_chars = 0; if (!shared_console_info[unit]) return input_error; @@ -1664,6 +1665,7 @@ fhandler_console::process_input_message (void) continue; } + num_chars += nread; if (toadd) { ssize_t ret; @@ -1681,15 +1683,22 @@ fhandler_console::process_input_message (void) goto out; } } + /* len == 0 if called from select.cc:peek_console() */ + if (len && num_chars >= len) + goto out; } out: + if (len == 0) + /* If len == 0, cancel reading from console input buffer. + Clear readahead buffer. */ + eat_readahead (-1); /* Discard processed recored. */ DWORD discard_len = min (total_read, i + 1); /* If input is signalled, do not discard input here because tcflush() is already called from line_edit(). */ if (stat == input_signalled && !(ti->c_lflag & NOFLSH)) discard_len = 0; - if (discard_len) + if (discard_len && (len || stat != input_ok)) { acquire_attach_mutex (mutex_timeout); DWORD resume_pid = attach_console (con.owner); diff --git a/winsup/cygwin/local_includes/fhandler.h b/winsup/cygwin/local_includes/fhandler.h index 1abc2ece84..2a7f8b3086 100644 --- a/winsup/cygwin/local_includes/fhandler.h +++ b/winsup/cygwin/local_includes/fhandler.h @@ -2324,7 +2324,7 @@ class fhandler_console: public fhandler_termios fh->copy_from (this); return fh; } - input_states process_input_message (); + input_states process_input_message (size_t len); bg_check_types bg_check (int sig, bool dontsignal = false); void setup_io_mutex (void); DWORD __acquire_input_mutex (const char *fn, int ln, DWORD ms); diff --git a/winsup/cygwin/release/3.6.10 b/winsup/cygwin/release/3.6.10 index c583e7746c..3f4b25abf5 100644 --- a/winsup/cygwin/release/3.6.10 +++ b/winsup/cygwin/release/3.6.10 @@ -22,3 +22,5 @@ Fixes: - Ensure the cons_master_thread runs only when it is really supposed to. - Fix NOFLSH behaviour in console a bit. + +- Fix typeahead input in console for bash. diff --git a/winsup/cygwin/select.cc b/winsup/cygwin/select.cc index 523c46ee64..b720834471 100644 --- a/winsup/cygwin/select.cc +++ b/winsup/cygwin/select.cc @@ -1172,7 +1172,7 @@ peek_console (select_record *me, bool) if (!r || !events_read) break; } - if (fhandler_console::input_winch == fh->process_input_message () + if (fhandler_console::input_winch == fh->process_input_message (0) && global_sigs[SIGWINCH].sa_handler != SIG_IGN && global_sigs[SIGWINCH].sa_handler != SIG_DFL) { From 36f40c9175013c58f7b7e51c1f7c3025f83a87e2 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 26 Jun 2026 09:16:49 +0200 Subject: [PATCH 019/102] Cygwin: console: re-enable the master thread before selecting cygwin input mode When a cygwin program and a non-cygwin program run in the same foreground process group (for example the pipeline `cat | ping`), Ctrl-C stopped interrupting the cygwin program after "Cygwin: console: Ensure the master thread runs only when it is supposed to". The console only delivers Ctrl-C as a raw 0x03 byte (which the console master thread reads and turns into a SIGINT for the foreground process group) while that thread is live. When it is suspended or disabled, set_input_mode (tty::cygwin) instead requests ENABLE_PROCESSED_INPUT, so the console raises a CTRL_C_EVENT and the 0x03 byte never reaches the master thread. The referenced commit reordered the two explicit enable paths, bg_check () and post_open_setup (), and one path which normally(*1) enables master thread, clearnup_for_non_cygwin_app(), so that set_input_mode (tty::cygwin) runs while disable_master_thread is still set; that leaves ENABLE_PROCESSED_INPUT on and the cygwin program never receives its SIGINT. (*1 ... except the process which calls exec() for non-cygwin app while itself is the cons master. In this case, subsequent set_input_mode() call sets the mode to tty::restore, and the master thread should be kept 'disabled' until the process exits.) Clear disable_master_thread before selecting cygwin input mode in those two paths, so the mode is configured with the master thread already live and ENABLE_PROCESSED_INPUT stays off. The disable paths and the synchronous suspension that the referenced commit added are left unchanged, so non-cygwin programs still get the master thread reliably suspended. Fixes: 733d5a953fa9 ("Cygwin: console: Ensure the master thread runs only when it is supposed to") Assisted-by: Opus 4.8 Signed-off-by: Johannes Schindelin Co-Authored-by: Takashi Yano Reviewed-by: Takashi Yano (cherry picked from commit 41e6325ad64fa00f5e61e25c2020a8bc716c29a0) --- winsup/cygwin/fhandler/console.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winsup/cygwin/fhandler/console.cc b/winsup/cygwin/fhandler/console.cc index c7c3839a56..dc1eb06443 100644 --- a/winsup/cygwin/fhandler/console.cc +++ b/winsup/cygwin/fhandler/console.cc @@ -949,12 +949,12 @@ fhandler_console::cleanup_for_non_cygwin_app (handle_set_t *p) termios *ti = shared_console_info[unit] ? &(shared_console_info[unit]->tty_min_state.ti) : &dummy; /* Cleaning-up console mode for non-cygwin app. */ + set_disable_master_thread (con.owner == GetCurrentProcessId ()); /* conmode can be tty::restore when non-cygwin app is exec'ed from login shell. */ tty::cons_mode conmode = cons_mode_on_close (p); set_output_mode (conmode, ti, p); set_input_mode (conmode, ti, p); - set_disable_master_thread (con.owner == GetCurrentProcessId ()); } /* Return the tty structure associated with a given tty number. If the @@ -1147,8 +1147,8 @@ fhandler_console::bg_check (int sig, bool dontsignal) in the same process group. */ if (sig == SIGTTIN && con.curr_input_mode != tty::cygwin) { - set_input_mode (tty::cygwin, &tc ()->ti, get_handle_set ()); set_disable_master_thread (false, this); + set_input_mode (tty::cygwin, &tc ()->ti, get_handle_set ()); } if (sig == SIGTTOU && con.curr_output_mode != tty::cygwin) set_output_mode (tty::cygwin, &tc ()->ti, get_handle_set ()); @@ -2047,8 +2047,8 @@ fhandler_console::post_open_setup (int fd) /* Setting-up console mode for cygwin app started from non-cygwin app. */ if (fd == 0) { - set_input_mode (tty::cygwin, &get_ttyp ()->ti, &handle_set); set_disable_master_thread (false, this); + set_input_mode (tty::cygwin, &get_ttyp ()->ti, &handle_set); } else if (fd == 1 || fd == 2) set_output_mode (tty::cygwin, &get_ttyp ()->ti, &handle_set); From c31308936b67cb07bb0fdc45c8ba16e8bfdf0787 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Tue, 30 Jun 2026 11:11:31 +0900 Subject: [PATCH 020/102] Cygwin: console: Correct previous NOFLSH fix The previous fix for NOFLSH mode does not work as intended. discard_key_events(), added in "Cygwin: console: Fix NOFLSH behaviour a bit", loops on ReadConsoleInputW() until it has consumed the requested number of records, but ReadConsoleInputW() blocks while the console input buffer is empty. sigflush() calls it with a hard-coded count of one and no guarantee that a record is actually queued: in the master-thread path the signalling record has already been read out of the buffer before sigflush() runs, so the call blocks until, and then swallows, the user's next keystroke. To avoid this, this patch does not discard input when process_sigs() is called from cons_master_thread, where the value of `fh` is NULL, because discarding will be done in cons_master_thread. And because the ReadConsoleInputW() return value is unchecked, a failed read leaves the count indeterminate, so "n -= n1" can underflow and spin. Check return value of ReadConsoleInputW() and abort if it fails. Moreover, discard_key_event(1) does not work as intended if the first key event is not a bKeyDown event correspoding to the signalling key. Use discard_key_events(0) instead. This means discarding input events to the current position processed. Since the key-strokes prior to the signalling key are already in the readahead buffer, so this call discards only the signalling key. The important point here is to discard input before releasing input_mutex by release_input_mutex_if_necessary(), because, if not, cons_master_thread starts to process key events before discarding signalling key event because the thread can acquire input_mutex. This causes the signalling key is processed twice. One separate point: the `process_input_message()` caller wraps `discard_key_events()` in `acquire_attach_mutex()` + `attach_console (con.owner)`, but the `sigflush()` call site does not, so the `ReadConsoleInputW()` there runs against whatever console the calling process happens to be attached to. With the guard above the worst case is a no-op when the calling process happens not to be attached, so it would be more correct to move the attach into the helper itself. This patch also fixes two more special cases. One is done_with_debugger case. When `gdb cat` is executed and the `cat` is running, Ctrl-C discards all the key events including the events after Ctrl-C. This is because tcflush() is used for the purpose. Use discard_key_events(0) instead. The other case is not_signalled_but_done case. Previously, when `cat | non-cygwin-app` is executed and Ctrl-C is pressed, but the `Ctrl-C` is not VINTR, line_edit() wrongly returned line_edit_signalled even though `cat` is not signalled by Ctrl-C. In this case, `cat` should receive Ctrl-C as a input char, while `non-cygwin-app` has been killed by Ctrl-C. Fix this in line_edit(). In the case of not_signalled_but_done case, setting `sawsig` flag and releasing `output_stopped` has been skipped with this patch, because this (Ctrl-C) is not a signal key in the case above. Fixes: 66324edf64a9 ("Cygwin: console: Fix NOFLSH behaviour a bit") Co-authored-by: Johannes Schindelin Signed-off-by: Takashi Yano Reviewed-by: Johannes Schindelin (cherry picked from commit 0d516c2b1f4d7e4abcf4be55056b5cd87f566e5e) --- winsup/cygwin/fhandler/console.cc | 28 ++++++++++++++--------- winsup/cygwin/fhandler/termios.cc | 30 +++++++++++++++---------- winsup/cygwin/local_includes/fhandler.h | 1 + 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/winsup/cygwin/fhandler/console.cc b/winsup/cygwin/fhandler/console.cc index dc1eb06443..3de1bcadad 100644 --- a/winsup/cygwin/fhandler/console.cc +++ b/winsup/cygwin/fhandler/console.cc @@ -1665,6 +1665,7 @@ fhandler_console::process_input_message (size_t len) continue; } + num_input_events_processed = i + 1; num_chars += nread; if (toadd) { @@ -1695,17 +1696,11 @@ fhandler_console::process_input_message (size_t len) /* Discard processed recored. */ DWORD discard_len = min (total_read, i + 1); /* If input is signalled, do not discard input here because - tcflush() is already called from line_edit(). */ - if (stat == input_signalled && !(ti->c_lflag & NOFLSH)) + discard_key_events() is already called from line_edit(). */ + if (stat == input_signalled) discard_len = 0; if (discard_len && (len || stat != input_ok)) - { - acquire_attach_mutex (mutex_timeout); - DWORD resume_pid = attach_console (con.owner); - discard_key_events (discard_len); - detach_console (resume_pid, con.owner); - release_attach_mutex (); - } + discard_key_events (discard_len); return stat; } @@ -1713,15 +1708,25 @@ void fhandler_console::discard_key_events (size_t n) { DWORD discarded = 0; + if (n == 0) + { + n = num_input_events_processed; + num_input_events_processed = 0; + } INPUT_RECORD input_rec[INREC_SIZE]; DWORD n1 = min (INREC_SIZE, n); + acquire_attach_mutex (mutex_timeout); + DWORD resume_pid = attach_console (con.owner); while (n) { - ReadConsoleInputW (get_handle (), input_rec, n1, &n1); + if (!ReadConsoleInputW (get_handle (), input_rec, n1, &n1) || !n1) + break; n -= n1; discarded += n1; n1 = min (INREC_SIZE, n); } + detach_console (resume_pid, con.owner); + release_attach_mutex (); con.num_processed -= min (con.num_processed, discarded); } @@ -2305,7 +2310,8 @@ fhandler_console::tcgetattr (struct termios *t) fhandler_console::fhandler_console (fh_devices devunit) : fhandler_termios (), input_ready (false), thread_sync_event (NULL), - input_mutex (NULL), output_mutex (NULL), unit (MAX_CONS_DEV) + input_mutex (NULL), output_mutex (NULL), unit (MAX_CONS_DEV), + num_input_events_processed (0) { dev_referred_via = (dev_t) devunit; if (devunit > 0) diff --git a/winsup/cygwin/fhandler/termios.cc b/winsup/cygwin/fhandler/termios.cc index 6508078505..e16b29619b 100644 --- a/winsup/cygwin/fhandler/termios.cc +++ b/winsup/cygwin/fhandler/termios.cc @@ -353,7 +353,10 @@ fhandler_termios::process_sigs (char c, tty* ttyp, fhandler_termios *fh) fhandler_pty_common::attach_console_temporarily (p->dwProcessId); if (fh && p == myself && being_debugged ()) { /* Avoid deadlock in gdb on console. */ - fh->tcflush(TCIFLUSH); + if (fh->is_console ()) + fh->discard_key_events (0 /* to current position */); + else + fh->tcflush(TCIFLUSH); fh->release_input_mutex_if_necessary (); } /* CTRL_C_EVENT does not work for the process started with @@ -444,10 +447,14 @@ fhandler_termios::process_sigs (char c, tty* ttyp, fhandler_termios *fh) goto not_a_sig; termios_printf ("got interrupt %d, sending signal %d", c, sig); - if (!(ti.c_lflag & NOFLSH) && fh) + if (fh) { - fh->eat_readahead (-1); - fh->discard_input (); + if (!(ti.c_lflag & NOFLSH)) + { + fh->eat_readahead (-1); + fh->discard_input (); + } + fh->discard_key_events (0 /* to current position */); } if (fh) fh->release_input_mutex_if_necessary (); @@ -460,6 +467,8 @@ fhandler_termios::process_sigs (char c, tty* ttyp, fhandler_termios *fh) not_a_sig: if ((ti.c_lflag & ISIG) && need_discard_input) { + if (need_send_sig) + return not_signalled; if (!(ti.c_lflag & NOFLSH) && fh) { fh->eat_readahead (-1); @@ -525,10 +534,11 @@ fhandler_termios::line_edit (const char *rptr, size_t nread, termios& ti, switch (process_sigs (c, get_ttyp (), this)) { case signalled: - case not_signalled_but_done: case done_with_debugger: sawsig = true; get_ttyp ()->output_stopped = false; + fallthrough; + case not_signalled_but_done: continue; case not_signalled_with_nat_reader: disable_eof_key = true; @@ -666,13 +676,9 @@ fhandler_termios::sigflush () be NULL while this is alive. However, we can conceivably close a ctty while exiting and that will zero this. */ if ((!have_execed || have_execed_cygwin) && tc () - && (tc ()->getpgid () == myself->pgid)) - { - if (!(tc ()->ti.c_lflag & NOFLSH)) - tcflush (TCIFLUSH); - else - discard_key_events (1); - } + && (tc ()->getpgid () == myself->pgid) + && !(tc ()->ti.c_lflag & NOFLSH)) + tcflush (TCIFLUSH); } pid_t diff --git a/winsup/cygwin/local_includes/fhandler.h b/winsup/cygwin/local_includes/fhandler.h index 2a7f8b3086..0a3e35db84 100644 --- a/winsup/cygwin/local_includes/fhandler.h +++ b/winsup/cygwin/local_includes/fhandler.h @@ -2200,6 +2200,7 @@ class fhandler_console: public fhandler_termios HANDLE input_mutex, output_mutex; handle_set_t handle_set; _minor_t unit; + size_t num_input_events_processed; /* Used when we encounter a truncated multi-byte sequence. The lead bytes are stored here and revisited in the next write call. */ From fa1e820cab741c4e38839cf9613b4eee4d9e4281 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Tue, 30 Jun 2026 14:12:38 +0900 Subject: [PATCH 021/102] Cygwin: pty: Do not transfer input to nat-pipe while masked On the command "cat | non-cygwin-app", `cat` sometimes fails to read key input. This happens when `cat` starts to read input before `non- cygwin-app` configures pseudo console. This is because pipe state is switched to nat-pipe when pseudo console is configured. This patch prevent the pipe state from changing to nat-pipe state if some cygwin process is reading input from the cyg-pipe. Fixes: f20641789427 ("Cygwin: pty: Reduce unecessary input transfer.") Signed-off-by: Takashi Yano Reviewed-by: Johannes Schindelin (cherry picked from commit 4c0fc56cad9d39afbacebcc58d2174d1af131b2c) --- winsup/cygwin/fhandler/pty.cc | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index d27ba4e59d..387bc03f38 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -998,6 +998,10 @@ fhandler_pty_slave::open_setup (int flags) void fhandler_pty_slave::cleanup () { + fhandler_pty_slave *arch = (fhandler_pty_slave *) archetype ? : this; + while (arch->num_reader) + mask_switch_to_nat_pipe (false, false); + /* This used to always call fhandler_pty_common::close when we were execing but that caused multiple closes of the handles associated with this pty. Since close_all_files is not called until after the cygwin process has @@ -1255,19 +1259,23 @@ fhandler_pty_slave::write (const void *ptr, size_t len) void fhandler_pty_slave::mask_switch_to_nat_pipe (bool mask, bool xfer) { + /* This input_mutex guard works as expected only because every + caller of transfer_input() holds input_mutex. This is a non- + local precondition. */ + WaitForSingleObject (input_mutex, mutex_timeout); char name[MAX_PATH]; shared_name (name, TTY_SLAVE_READING, get_minor ()); HANDLE masked = OpenEvent (READ_CONTROL, FALSE, name); CloseHandle (masked); - WaitForSingleObject (input_mutex, mutex_timeout); + fhandler_pty_slave *arch = (fhandler_pty_slave *) archetype ? : this; if (mask) { - if (InterlockedIncrement (&num_reader) == 1) - slave_reading = CreateEvent (&sec_none_nih, TRUE, FALSE, name); + if (InterlockedIncrement (&arch->num_reader) == 1) + arch->slave_reading = CreateEvent (&sec_none_nih, TRUE, FALSE, name); } - else if (InterlockedDecrement (&num_reader) == 0) - CloseHandle (slave_reading); + else if (InterlockedDecrement (&arch->num_reader) == 0) + CloseHandle (arch->slave_reading); if (!!masked != mask && xfer && get_ttyp ()->switch_to_nat_pipe) { @@ -4120,6 +4128,18 @@ fhandler_pty_slave::transfer_input (tty::xfer_dir dir, HANDLE from, tty *ttyp, HANDLE input_available_event, HANDLE input_transferred_to_cyg) { + if (dir == tty::to_nat) + { + char name[MAX_PATH]; + shared_name (name, TTY_SLAVE_READING, ttyp->get_minor ()); + HANDLE masked = OpenEvent (READ_CONTROL, FALSE, name); + CloseHandle (masked); + if (masked) + /* Cygwin process is reading cyg-pipe. + Do not transfer input to nat-pipe. */ + return; + } + HANDLE to; if (dir == tty::to_nat) to = ttyp->to_slave_nat (); From fb1d831e6c06606c3a36565ef28e7388f63c38cf Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Wed, 8 Jul 2026 21:54:45 +0900 Subject: [PATCH 022/102] Cygwin: pty: Fix nat_pipe_owner_pid when gdb runs non-cygwin app Previously, nat_pipe_owner_pid was incorrectly set to 0 when the inferior of gdb was a non-cygwin app. Due to this bug, repeatedly running a non-cygwin app under gdb could lead to an unexpected crash. This occurred because the previous code in setup_for_non_cygwin_app() set nat_pipe_owner_pid to exec_dwProcessId, which is correct when the caller is the stub process of the non-cygwin app. exec_dwProcessId is the PID of the stub process, and dwProcessId is the PID of the spawned process in the stub process. However, when the caller is gdb, since the owner should be gdb itself, nat_pipe_owner_pid must be set to myself->dwProcessId where the normal process's PID is stored. With this fix, attach_console_temporarily() can be called with target pid equal to the process's own pid in gdb, in which case the attach operation is skipped. Note that this issue commonly occurs in programs that launch non- cygwin app directly via CreateProcess(), as gdb does. Fixes: 1e6c51d74136 ("Cygwin: pty: Reorganize the code path of setting up and closing pcon.") Signed-off-by: Takashi Yano Reviewed-by: Johannes Schindelin (cherry picked from commit 50f4ff48727f138bded3f1c4e41357b46df1ef95) --- winsup/cygwin/fhandler/pty.cc | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index 387bc03f38..c5081bcaf3 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -3689,7 +3689,8 @@ fhandler_pty_slave::setup_pseudoconsole () replace_nat_handles (hpConIn, hpConOut); if (!process_alive (get_ttyp ()->nat_pipe_owner_pid)) - get_ttyp ()->nat_pipe_owner_pid = myself->exec_dwProcessId; + get_ttyp ()->nat_pipe_owner_pid = + myself->exec_dwProcessId ? : myself->dwProcessId; if (hpcon && nat_pipe_owner_self (get_ttyp ()->nat_pipe_owner_pid)) { @@ -4397,7 +4398,12 @@ fhandler_pty_slave::setup_for_non_cygwin_app (bool nopcon, fhandler_pty_slave *ptys = (fhandler_pty_slave *) fh; ptys->get_ttyp ()->switch_to_nat_pipe = true; if (!process_alive (ptys->get_ttyp ()->nat_pipe_owner_pid)) - ptys->get_ttyp ()->nat_pipe_owner_pid = myself->exec_dwProcessId; + /* In normal case where the current process is the stub process for + non-cygwin app, set owner to exec_dwProcessId (which is the PID + of the stub process itself). In gdb case, since gdb itself + should be the owner, the owner pid must be set to dwProcessId. */ + ptys->get_ttyp ()->nat_pipe_owner_pid = + myself->exec_dwProcessId ? : myself->dwProcessId; } bool pcon_enabled = false; if (!nopcon) @@ -4525,6 +4531,8 @@ fhandler_pty_common::attach_console_temporarily (DWORD target_pid) { DWORD resume_pid = 0; acquire_attach_mutex (mutex_timeout); + if (target_pid == GetCurrentProcessId ()) + return target_pid; pinfo pinfo_resume (myself->ppid); if (pinfo_resume) resume_pid = pinfo_resume->dwProcessId; @@ -4543,6 +4551,11 @@ fhandler_pty_common::attach_console_temporarily (DWORD target_pid) void fhandler_pty_common::resume_from_temporarily_attach (DWORD resume_pid) { + if (resume_pid == GetCurrentProcessId ()) + { + release_attach_mutex (); + return; + } bool console_exists = (resume_pid != (DWORD) -1); if (!console_exists || resume_pid) { From b11613e477c006b2ce0332463ed07f1118260e79 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Thu, 9 Jul 2026 21:30:35 +0900 Subject: [PATCH 023/102] Update release note 3.6.10 --- winsup/cygwin/release/3.6.10 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/winsup/cygwin/release/3.6.10 b/winsup/cygwin/release/3.6.10 index 3f4b25abf5..825ed3a625 100644 --- a/winsup/cygwin/release/3.6.10 +++ b/winsup/cygwin/release/3.6.10 @@ -24,3 +24,9 @@ Fixes: - Fix NOFLSH behaviour in console a bit. - Fix typeahead input in console for bash. + +- Fix a pty race issue at startup of cygwin and non-cygwin processes + where the different pipe mode (nat or cyg) is required. + +- Fix non-cygwin inferior crash in gdb when it is launched repeatedly + in a pty. From b9c42477004de85d75623f4c0b228cb8bb577ebc Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 30 Apr 2026 15:04:04 +0000 Subject: [PATCH 024/102] Cygwin: pty: detect pcon-backed pty for non-Cygwin-spawned children When a Cygwin process (e.g. `bash` under MinTTY) spawns a native Win32 child (e.g. `git.exe`) with pseudo console support enabled, the child gets a pseudo console that bridges the pty. If that native child then spawns a Cygwin grandchild (e.g. `vim`, `less`), the grandchild inherits the pseudo console's console handles. In `init_std_file_from_handle()`, the grandchild's msys2-runtime sees `GetConsoleScreenBufferInfo()` succeed on those handles and, with no valid `ctty` set, falls back to `FH_CONSOLE` and gives the process `cons0` instead of connecting to the pty. This causes scrollback clobbering in MinTTY because alternate screen sequences (`ESC[?1049h` / `ESC[?1049l`) are handled by `fhandler_console`'s `save_restore()` against the pseudo console's buffer, which has no correspondence to MinTTY's scrollback. Fix this in the existing console branch of `init_std_file_from_handle()`: when there is no valid `ctty` and we are about to fall back to `FH_CONSOLE`, first scan the shared tty table for an entry whose `pcon_activated` is set and whose `nat_pipe_owner_pid` is in our console's process list (via `GetConsoleProcessList`). If found, parse the device as that pty slave instead of as a real console. The handle is closed in either fallback path, matching the existing `FH_CONSOLE` behavior. `myself->ctty` is left untouched; the regular `fhandler_pty_slave::open_setup()` path will set it via `myself->set_ctty()` when the pty slave is opened. The structure of `find_pcon_pty()` matters and is easy to get wrong in case a keen developer would like to refactor this code in the future. This code runs on every Cygwin process startup whose parent is non-Cygwin, so the common path (no pty with an active pseudo console) must remain free of expensive operations. Two pitfalls to avoid: filtering tty entries with `tty::exists()` looks correct but creates and destroys a named pipe per entry (128 entries on every call), and hoisting the `GetConsoleProcessList()` call out of the loop pays the cross-process cost even when no candidate exists. The current shape, a cheap shared-memory boolean check first and a lazily fetched process list only on the first candidate, keeps the common case at a handful of pointer reads. Reported downstream at https://github.com/git-for-windows/git/issues/5303 and bisected to a Git for Windows release that upgraded the bundled msys2-runtime from 3.3.6 (no pseudo console code) to 3.4.6 (the new pseudo console architecture). Fixes: bb4285206207 ("Cygwin: pty: Implement new pseudo console support.") Assisted-by: Claude Opus 4.7 (1M context) Signed-off-by: Johannes Schindelin Reviewed-by: Takashi Yano (cherry picked from commit 6eed1ef74869e113c01c97a226fb11a1bad21e40) --- winsup/cygwin/dtable.cc | 12 +++++++++- winsup/cygwin/local_includes/tty.h | 5 ++++ winsup/cygwin/tty.cc | 37 ++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/winsup/cygwin/dtable.cc b/winsup/cygwin/dtable.cc index 7303f7eacc..ce29f46082 100644 --- a/winsup/cygwin/dtable.cc +++ b/winsup/cygwin/dtable.cc @@ -327,7 +327,17 @@ dtable::init_std_file_from_handle (int fd, HANDLE handle) dev.parse (myself->ctty); else { - dev.parse (FH_CONSOLE); + /* Check whether the inherited console is actually a pseudo + console bridging a pty. This happens when our non-Cygwin + parent was itself spawned by a Cygwin process from a pty + (e.g. bash spawning git.exe which then spawns vim). In + that case, connect to the pty slave instead of treating + the handle as a real console. */ + int pcon_minor = cygwin_shared->tty.find_pcon_pty (); + if (pcon_minor >= 0) + dev.parse (FHDEV (DEV_PTYS_MAJOR, pcon_minor)); + else + dev.parse (FH_CONSOLE); CloseHandle (handle); handle = INVALID_HANDLE_VALUE; } diff --git a/winsup/cygwin/local_includes/tty.h b/winsup/cygwin/local_includes/tty.h index a03e965e4d..185c41f68c 100644 --- a/winsup/cygwin/local_includes/tty.h +++ b/winsup/cygwin/local_includes/tty.h @@ -176,6 +176,10 @@ class tty: public tty_min void wait_fwd (); bool pty_input_state_eq (xfer_dir x) { return pty_input_state == x; } bool nat_fg (pid_t pgid); + bool has_active_pcon () const + { return pcon_activated && switch_to_nat_pipe; } + bool has_pcon_and_owner (DWORD pid) const + { return pcon_activated && switch_to_nat_pipe && nat_pipe_owner_pid == pid; } friend class fhandler_pty_common; friend class fhandler_pty_master; friend class fhandler_pty_slave; @@ -194,6 +198,7 @@ class tty_list int connect (int); void init (); tty_min *get_cttyp (); + int find_pcon_pty (); int attach (int n); static void init_session (); friend class lock_ttys; diff --git a/winsup/cygwin/tty.cc b/winsup/cygwin/tty.cc index c8730e81c5..5cce05de34 100644 --- a/winsup/cygwin/tty.cc +++ b/winsup/cygwin/tty.cc @@ -123,6 +123,43 @@ tty_list::init () } } +/* Search for a pty whose pseudo console owns our console. + Return tty minor number or -1 if not found. + Called from init_std_file_from_handle() for processes started by + non-Cygwin parents to detect that inherited console handles are + from a pcon-backed pty. + + The cheap precondition (any tty with pcon active in shared memory) + short-circuits the common case where no pty has a pseudo console + active, avoiding the GetConsoleProcessList() LPC call entirely. */ +int +tty_list::find_pcon_pty () +{ + DWORD pids[128]; + DWORD count = 0; + bool got_pids = false; + + for (int i = 0; i < NTTYS; i++) + { + if (!ttys[i].has_active_pcon ()) + continue; + + /* Fetch the console process list lazily, only on first candidate. */ + if (!got_pids) + { + count = GetConsoleProcessList (pids, 128); + if (!count) + return -1; + got_pids = true; + } + + for (DWORD j = 0; j < count; j++) + if (ttys[i].has_pcon_and_owner (pids[j])) + return i; + } + return -1; +} + /* Search for a free tty and allocate it. Return tty number or -1 if error. */ From 7a68b7c60c5bcf90afe619c2ee2342357b355320 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Sat, 13 Jun 2026 21:48:19 +0900 Subject: [PATCH 025/102] Cygwin: pty: Discard pcon input buffer when discard_input is called Previously, the process on pty could not be a child of non-cygwin process. In that case, it is not necessary to flush pcon input buffer even when discard_input() is called. However, now, the child process of non-cygwin app on pseudo console is running on pty. So, discard_input() should affect to the pcon input buffer as well. This prevents the problem wihch can be demonstrated by: 1) Run 'sleep 10' in cmd.exe 2) Enter 'ps\n' while sleeping 3) Press Ctrl-C 4) 'ps' will be executed after terminating 'sleep' by Ctrl-C. Signed-off-by: Takashi Yano Reviwed-by: Mark Geisert (cherry picked from commit ee929717bd09bd5612419e971cba7b6370039431) --- winsup/cygwin/fhandler/pty.cc | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index c5081bcaf3..a0604df047 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -418,6 +418,14 @@ fhandler_pty_master::discard_input () if (!get_ttyp ()->pcon_activated) while (::bytes_available (bytes_in_pipe, from_master_nat) && bytes_in_pipe) ReadFile (from_master_nat, buf, sizeof(buf), &n, NULL); + else + { + DWORD target_pid = get_ttyp ()->nat_pipe_owner_pid; + DWORD resume_pid = + fhandler_pty_common::attach_console_temporarily (target_pid); + FlushConsoleInputBuffer (h_pcon_in_dupped); + fhandler_pty_common::resume_from_temporarily_attach (resume_pid); + } get_ttyp ()->discard_input = true; ReleaseMutex (input_mutex); } @@ -2410,7 +2418,8 @@ fhandler_pty_master::write (const void *ptr, size_t len) for (size_t i = 0, j = 0; i < len; i++) { process_sig_state r = process_sigs (buf[i], get_ttyp (), this); - if (r != done_with_debugger) + if (r != done_with_debugger && + (r != signalled || (ti.c_lflag & NOFLSH) || buf[i] == '\003')) { char c = buf[i]; /* Workaround for pseudo console in Windows 11 */ From 1187e92829347227ed65357b1c581c92c46f2dff Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Sat, 13 Jun 2026 22:02:14 +0900 Subject: [PATCH 026/102] Cygwin: pty: Fixup pty state after a cygwin app exits Previously, the cygwin process on pty is always a child of another cygwin app on pty. If a cygwin app is a child of non-cygwin app in pseudo console, it was running on console originating from pseudo console. Now, the child of a non-cygwin app on pseudo console is running on pty, so, it is necessary to restore the pty state to the state where the parent process is running. This patch does the following fixup when the cygwin process on pty exits: 1) Switch pipe mode from cyg-pipe to nat-pipe. 2) Notify the current cursor position to pseudo console These prevent the problems: 1) Run 'cat' in cmd.exe and stop it by Ctrl-C. After that cmd.exe cannot receive key input. 2) Run 'ps' in cmd.exe. The cursor position will not be maintained correctly after that. Signed-off-by: Takashi Yano Reviewed-by: Mark Geisert (cherry picked from commit b34394d456b6d46fc112273183fc0cae5a613e18) --- winsup/cygwin/fhandler/pty.cc | 79 +++++++++++++++++++++++-- winsup/cygwin/local_includes/fhandler.h | 2 + winsup/cygwin/local_includes/tty.h | 1 + 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index a0604df047..33efbf02b0 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -223,6 +223,52 @@ atexit_func (void) } } +void +fhandler_pty_slave::req_fixup_pcon_state (void) +{ + while (true) + { + WaitForSingleObject (input_mutex, mutex_timeout); + if (!get_ttyp ()->pcon_start_pid) + break; + /* Another request is on going. */ + ReleaseMutex (input_mutex); + yield (); + } + + DWORD n; + /* indicates that this "ESC[6n" is just for fixing-up cursor position */ + get_ttyp ()->req_fixup_pcon_cur_pos = true; + get_ttyp ()->req_xfer_input = true; /* indicates that this "ESC[6n" + is just for transfer input */ + get_ttyp ()->pcon_start = true; + get_ttyp ()->pcon_start_pid = myself->pid; + WriteFile (get_output_handle (), "\033[6n", 4, &n, NULL); + ReleaseMutex (input_mutex); + while (get_ttyp ()->pcon_start_pid) + /* wait for completion of fixing-up in master::write(). */ + yield (); +} + +void +fhandler_pty_master::fixup_pcon_cursor_position (int x, int y) +{ + HANDLE pcon_owner = OpenProcess (PROCESS_DUP_HANDLE, FALSE, + get_ttyp ()->nat_pipe_owner_pid); + HANDLE h_pcon_out = NULL; + DuplicateHandle (pcon_owner, get_ttyp ()->h_pcon_out, + GetCurrentProcess (), &h_pcon_out, + 0, TRUE, DUPLICATE_SAME_ACCESS); + CloseHandle (pcon_owner); + DWORD target_pid = get_ttyp ()->nat_pipe_owner_pid; + DWORD resume_pid = + fhandler_pty_common::attach_console_temporarily (target_pid); + COORD cur_pos = {(SHORT) (x - 1), (SHORT) (y - 1)}; + SetConsoleCursorPosition (h_pcon_out, cur_pos); + fhandler_pty_common::resume_from_temporarily_attach (resume_pid); + CloseHandle (h_pcon_out); +} + #define DEF_HOOK(name) static __typeof__ (name) *name##_Orig /* CreateProcess() is hooked for GDB etc. */ DEF_HOOK (CreateProcessA); @@ -997,6 +1043,19 @@ fhandler_pty_slave::open (int flags, mode_t) bool fhandler_pty_slave::open_setup (int flags) { + if (get_ttyp ()->pcon_activated) + { + HANDLE pcon_owner = OpenProcess (PROCESS_DUP_HANDLE, FALSE, + get_ttyp ()->nat_pipe_owner_pid); + DuplicateHandle (pcon_owner, get_ttyp ()->h_pcon_in, + GetCurrentProcess (), &get_handle_nat (), + 0, TRUE, DUPLICATE_SAME_ACCESS); + DuplicateHandle (pcon_owner, get_ttyp ()->h_pcon_out, + GetCurrentProcess (), &get_output_handle_nat (), + 0, TRUE, DUPLICATE_SAME_ACCESS); + CloseHandle (pcon_owner); + } + set_flags ((flags & ~O_TEXT) | O_BINARY); myself->set_ctty (this, flags); report_tty_counts (this, "opened", ""); @@ -1010,6 +1069,9 @@ fhandler_pty_slave::cleanup () while (arch->num_reader) mask_switch_to_nat_pipe (false, false); + if (get_ttyp ()->pcon_activated && get_ttyp ()->getpgid () == myself->pgid) + req_fixup_pcon_state (); + /* This used to always call fhandler_pty_common::close when we were execing but that caused multiple closes of the handles associated with this pty. Since close_all_files is not called until after the cygwin process has @@ -2301,10 +2363,21 @@ fhandler_pty_master::write (const void *ptr, size_t len) state = 2; if (state == 2) { - /* req_xfer_input is true if "ESC[6n" was sent just for + /* req_fixup_pcon_cur_pos is true if "ESC[6n" was sent + for requesting cursor-position-fixup that is needed + when a non-cygwin app executes a cygwin app and the + cygwin app exits. + req_xfer_input is true if "ESC[6n" was sent just for triggering transfer_input() in master. In this case, the response sequence should not be written. */ - if (!get_ttyp ()->req_xfer_input) + if (get_ttyp ()->req_fixup_pcon_cur_pos) + { + int x, y; + sscanf (wpbuf, "\033[%d;%dR", &y, &x); + fixup_pcon_cursor_position (x, y); + get_ttyp ()->req_fixup_pcon_cur_pos = false; + } + else if (!get_ttyp ()->req_xfer_input) WriteFile (to_slave_nat, wpbuf, ixput, &n, NULL); ixput = 0; state = 0; @@ -3921,8 +3994,6 @@ fhandler_pty_slave::close_pseudoconsole (tty *ttyp, DWORD force_switch_to) ttyp->pcon_activated = false; ttyp->switch_to_nat_pipe = false; ttyp->nat_pipe_owner_pid = 0; - ttyp->pcon_start = false; - ttyp->pcon_start_pid = 0; } if (ttyp->pcon_handle_ready_event) { diff --git a/winsup/cygwin/local_includes/fhandler.h b/winsup/cygwin/local_includes/fhandler.h index 0a3e35db84..d7a41edaa7 100644 --- a/winsup/cygwin/local_includes/fhandler.h +++ b/winsup/cygwin/local_includes/fhandler.h @@ -2530,6 +2530,7 @@ class fhandler_pty_slave: public fhandler_pty_common void setpgid_aux (pid_t pid); static void release_ownership_of_nat_pipe (tty *ttyp, fhandler_termios *fh); void replace_nat_handles (HANDLE new_input, HANDLE new_output); + void req_fixup_pcon_state (void); }; #define __ptsname(buf, unit) __small_sprintf ((buf), "/dev/pty%d", (unit)) @@ -2637,6 +2638,7 @@ class fhandler_pty_master: public fhandler_pty_common void apply_line_edit_to_transferred_input (); line_edit_status line_edit_maybe (const char *p, size_t len, termios&, ssize_t *n); + void fixup_pcon_cursor_position (int x, int y); }; class fhandler_dev_null: public fhandler_base diff --git a/winsup/cygwin/local_includes/tty.h b/winsup/cygwin/local_includes/tty.h index 185c41f68c..f71201f48a 100644 --- a/winsup/cygwin/local_includes/tty.h +++ b/winsup/cygwin/local_includes/tty.h @@ -140,6 +140,7 @@ class tty: public tty_min xfer_dir pty_input_state; bool discard_input; bool stop_fwd_thread; + bool req_fixup_pcon_cur_pos; public: HANDLE from_master_nat () const { return _from_master_nat; } From 12c0b81d224816faa327aff1b5760d08f78f5318 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 29 May 2026 19:07:26 +0200 Subject: [PATCH 027/102] Cygwin: pty: grow GetConsoleProcessList buffer in find_pcon_pty() find_pcon_pty() was passing a fixed 128-DWORD stack array to GetConsoleProcessList(). If the calling Cygwin process happens to be attached to a console with more than 128 processes, the Win32 function returns the required size and the buffer contents are undefined; the existing if-zero check did not catch that case, so the subsequent loop walked uninitialised data and could either miss the candidate pty or, worse, match against junk PIDs and return the wrong tty index. Adopt the buffer-too-small dance from fhandler_termios::get_console_process_id() in winsup/cygwin/fhandler/termios.cc, which already had to solve this problem and which also notes that the new condrv does not accept oversized first-call buffers (https://github.com/microsoft/terminal/issues/18264#issuecomment-2515448548). The buffer comes from tmp_pathbuf so the same NT_MAX_PATH cap (currently 1024 DWORDs, i.e. 4096 processes) applies; we bail out with -1 if even that is not enough rather than allocate unbounded memory or guess. Bumping the start-with size from 1 would defeat the condrv work-around mentioned above, so we keep the same one-element initial probe as termios.cc and let the loop grow. Suggested-by: Takashi Yano Assisted-by: Opus 4.7 Signed-off-by: Johannes Schindelin (cherry picked from commit b65e1544d45567f0033c57a0aa1543c5e654950a) --- winsup/cygwin/tty.cc | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/winsup/cygwin/tty.cc b/winsup/cygwin/tty.cc index 5cce05de34..9bc2a084fb 100644 --- a/winsup/cygwin/tty.cc +++ b/winsup/cygwin/tty.cc @@ -19,6 +19,7 @@ details. */ #include "cygheap.h" #include "pinfo.h" #include "shared_info.h" +#include "tls_pbuf.h" HANDLE NO_COPY tty_list::mutex = NULL; @@ -135,7 +136,9 @@ tty_list::init () int tty_list::find_pcon_pty () { - DWORD pids[128]; + tmp_pathbuf tp; + DWORD *pids = (DWORD *) tp.c_get (); + const DWORD buf_size = NT_MAX_PATH / sizeof (DWORD); DWORD count = 0; bool got_pids = false; @@ -144,10 +147,20 @@ tty_list::find_pcon_pty () if (!ttys[i].has_active_pcon ()) continue; - /* Fetch the console process list lazily, only on first candidate. */ + /* Fetch the console process list lazily, only on first candidate. + The buffer-too-large dance mirrors the one in termios.cc's + get_console_process_id() and works around new condrv's dislike + of oversized first-call buffers, see + https://github.com/microsoft/terminal/issues/18264#issuecomment-2515448548 */ if (!got_pids) { - count = GetConsoleProcessList (pids, 128); + DWORD buf_size1 = 1; + while ((count = GetConsoleProcessList (pids, buf_size1)) > buf_size1) + { + if (count > buf_size) + return -1; + buf_size1 = count; + } if (!count) return -1; got_pids = true; From 57fd3f604d0b2261abd135ca522e3f728a579302 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 25 Jun 2026 13:41:42 +0200 Subject: [PATCH 028/102] Cygwin: pty: bound the cursor-sync round-trip so an exiting process cannot hang The cursor-position fixup added in "Cygwin: pty: Fixup pty state after a cygwin app exits" runs from cleanup() on every foreground Cygwin-app exit while a pseudo console is active, and it waits on two unbounded loops for the master to answer the "ESC[6n" it just sent: one that spins until the pcon_start_pid slot is free, and one that spins until the master clears the slot again. pcon_start_pid is only ever cleared once master::write() parses the terminal's reply, so if that reply never comes, because the terminal is going away, the forwarding pipe is broken, or a previous requester died mid-handshake, the exiting process spins on yield() forever and never exits. Bound both waits with a three second deadline using GetTickCount64(), and on timeout clear our own pcon_start_pid slot, but only if it is still ours, so a give-up does not stomp a later requester. Also restore the pcon_start and pcon_start_pid reset that the same commit removed from close_pseudoconsole(); it is the backstop that keeps a requester which died without clearing its slot from wedging the next one. The worst case is now a slightly stale cursor after a timeout rather than a process that refuses to exit. Fixes: b34394d456b6 ("Cygwin: pty: Fixup pty state after a cygwin app exits") Assisted-by: Opus 4.8 Signed-off-by: Johannes Schindelin Co-authored-by: Takashi Yano Reviewed-by: Takashi Yano --- winsup/cygwin/fhandler/pty.cc | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index 33efbf02b0..5baf96a89b 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -226,6 +226,7 @@ atexit_func (void) void fhandler_pty_slave::req_fixup_pcon_state (void) { + ULONGLONG deadline = GetTickCount64 () + 3000; while (true) { WaitForSingleObject (input_mutex, mutex_timeout); @@ -233,6 +234,10 @@ fhandler_pty_slave::req_fixup_pcon_state (void) break; /* Another request is on going. */ ReleaseMutex (input_mutex); + if (GetTickCount64 () > deadline) + /* A previous requester is stuck; give up this sync rather than + spin forever. */ + return; yield (); } @@ -245,9 +250,25 @@ fhandler_pty_slave::req_fixup_pcon_state (void) get_ttyp ()->pcon_start_pid = myself->pid; WriteFile (get_output_handle (), "\033[6n", 4, &n, NULL); ReleaseMutex (input_mutex); - while (get_ttyp ()->pcon_start_pid) + deadline = GetTickCount64 () + 3000; + while (get_ttyp ()->pcon_start_pid && GetTickCount64 () <= deadline) /* wait for completion of fixing-up in master::write(). */ yield (); + /* If the master never answered (e.g. the terminal is going away), + clear our own request so a stale pcon_start_pid cannot wedge the + next requester. */ + if (get_ttyp ()->pcon_start_pid == (pid_t) myself->pid) + { + WaitForSingleObject (input_mutex, mutex_timeout); + if (get_ttyp ()->pcon_start_pid == (pid_t) myself->pid) + { + get_ttyp ()->req_fixup_pcon_cur_pos = false; + get_ttyp ()->req_xfer_input = false; + get_ttyp ()->pcon_start = false; + get_ttyp ()->pcon_start_pid = 0; + } + ReleaseMutex (input_mutex); + } } void @@ -3994,6 +4015,13 @@ fhandler_pty_slave::close_pseudoconsole (tty *ttyp, DWORD force_switch_to) ttyp->pcon_activated = false; ttyp->switch_to_nat_pipe = false; ttyp->nat_pipe_owner_pid = 0; + /* Safety net: if a req_fixup_pcon_state() requester died without + clearing its slot, do not leave pcon_start_pid set forever. */ + if (ttyp->pcon_start_pid == myself->pid) + { + ttyp->pcon_start = false; + ttyp->pcon_start_pid = 0; + } } if (ttyp->pcon_handle_ready_event) { From 425004632e4bb42211c0a781e30255aff9204415 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 25 Jun 2026 13:41:43 +0200 Subject: [PATCH 029/102] Cygwin: pty: do not leak nat handles when adopting the pcon's in open_setup() When a Cygwin process opens a pty slave whose pseudo console is already active, open() has just installed duplicates of the cyg master-side pipe ends into io_handle_nat and output_handle_nat. The pcon adoption added in "Cygwin: pty: Fixup pty state after a cygwin app exits" overwrites those two slots via &get_handle_nat() / &get_output_handle_nat() without closing them first, so two handles leak on every pcon-backed grandchild open. It also hands the result of OpenProcess() straight to DuplicateHandle() without a NULL check, so if the nat-pipe owner has already exited both duplications fail and leave the nat slots NULL, which then breaks the slave's input routing. Close the old slots before replacing them, skip the replacement entirely when OpenProcess() returns NULL so we degrade to the handles open() installed, and make the pair transactional so a partial success cannot leave one original slot and one pcon slot. Fixes: b34394d456b6 ("Cygwin: pty: Fixup pty state after a cygwin app exits") Assisted-by: Opus 4.8 Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/pty.cc | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index 5baf96a89b..828493fabf 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -1068,13 +1068,33 @@ fhandler_pty_slave::open_setup (int flags) { HANDLE pcon_owner = OpenProcess (PROCESS_DUP_HANDLE, FALSE, get_ttyp ()->nat_pipe_owner_pid); - DuplicateHandle (pcon_owner, get_ttyp ()->h_pcon_in, - GetCurrentProcess (), &get_handle_nat (), - 0, TRUE, DUPLICATE_SAME_ACCESS); - DuplicateHandle (pcon_owner, get_ttyp ()->h_pcon_out, - GetCurrentProcess (), &get_output_handle_nat (), - 0, TRUE, DUPLICATE_SAME_ACCESS); - CloseHandle (pcon_owner); + if (pcon_owner) + { + HANDLE new_in = NULL, new_out = NULL; + bool ok_in = DuplicateHandle (pcon_owner, get_ttyp ()->h_pcon_in, + GetCurrentProcess (), &new_in, + 0, TRUE, DUPLICATE_SAME_ACCESS); + bool ok_out = DuplicateHandle (pcon_owner, get_ttyp ()->h_pcon_out, + GetCurrentProcess (), &new_out, + 0, TRUE, DUPLICATE_SAME_ACCESS); + if (ok_in && ok_out) + { + /* Close the cyg master-side handles open() installed before + replacing them, so they do not leak. */ + CloseHandle (get_handle_nat ()); + CloseHandle (get_output_handle_nat ()); + set_handle_nat (new_in); + set_output_handle_nat (new_out); + } + else + { + if (new_in) + CloseHandle (new_in); + if (new_out) + CloseHandle (new_out); + } + CloseHandle (pcon_owner); + } } set_flags ((flags & ~O_TEXT) | O_BINARY); From fc4fc3b2c39885ca347cf6757e17369b78e983d8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 25 Jun 2026 13:41:44 +0200 Subject: [PATCH 030/102] Cygwin: pty: validate the cursor-position reply before moving the pcon cursor The CSI6n reply handler added in "Cygwin: pty: Fixup pty state after a cygwin app exits" runs sscanf() on the terminal's response but ignores its return value, so a malformed or partial reply leaves the x and y locals uninitialised and hands them to SetConsoleCursorPosition(), which is exactly the cursor corruption the commit set out to prevent. Only call the fixup when sscanf() reports both coordinates parsed, and in fixup_pcon_cursor_position() clamp the coordinates into the valid SHORT range before the COORD cast so a stray reply cannot wrap into a negative position. While there, check OpenProcess() for NULL (the nat-pipe owner may have exited) and check the DuplicateHandle() result instead of using a possibly-NULL screen-buffer handle. Fixes: b34394d456b6 ("Cygwin: pty: Fixup pty state after a cygwin app exits") Assisted-by: Opus 4.8 Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/pty.cc | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index 828493fabf..3eb7f8a0a7 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -274,12 +274,23 @@ fhandler_pty_slave::req_fixup_pcon_state (void) void fhandler_pty_master::fixup_pcon_cursor_position (int x, int y) { + /* A malformed or out-of-range reply must not be turned into a wrapped + negative COORD. */ + if (x < 1 || y < 1 || x > 0x7fff || y > 0x7fff) + return; HANDLE pcon_owner = OpenProcess (PROCESS_DUP_HANDLE, FALSE, get_ttyp ()->nat_pipe_owner_pid); + if (!pcon_owner) + /* The nat-pipe owner is gone; nothing to sync to. */ + return; HANDLE h_pcon_out = NULL; - DuplicateHandle (pcon_owner, get_ttyp ()->h_pcon_out, - GetCurrentProcess (), &h_pcon_out, - 0, TRUE, DUPLICATE_SAME_ACCESS); + if (!DuplicateHandle (pcon_owner, get_ttyp ()->h_pcon_out, + GetCurrentProcess (), &h_pcon_out, + 0, TRUE, DUPLICATE_SAME_ACCESS)) + { + CloseHandle (pcon_owner); + return; + } CloseHandle (pcon_owner); DWORD target_pid = get_ttyp ()->nat_pipe_owner_pid; DWORD resume_pid = @@ -2414,8 +2425,8 @@ fhandler_pty_master::write (const void *ptr, size_t len) if (get_ttyp ()->req_fixup_pcon_cur_pos) { int x, y; - sscanf (wpbuf, "\033[%d;%dR", &y, &x); - fixup_pcon_cursor_position (x, y); + if (sscanf (wpbuf, "\033[%d;%dR", &y, &x) == 2) + fixup_pcon_cursor_position (x, y); get_ttyp ()->req_fixup_pcon_cur_pos = false; } else if (!get_ttyp ()->req_xfer_input) From c7fae92857ad15c94dd9b46a05b325b7ef9e17c2 Mon Sep 17 00:00:00 2001 From: Kaleb Barrett Date: Sun, 14 Mar 2021 18:58:55 -0500 Subject: [PATCH 031/102] Fix msys library name in import libraries Cygwin's speclib doesn't handle dashes or dots. However, we are about to rename the output file name from `cygwin1.dll` to `msys-2.0.dll`. Let's preemptively fix up all the import libraries that would link against `msys_2_0.dll` to correctly link against `msys-2.0.dll` instead. --- winsup/cygwin/scripts/speclib | 1 + 1 file changed, 1 insertion(+) diff --git a/winsup/cygwin/scripts/speclib b/winsup/cygwin/scripts/speclib index 41a3a8e139..42a02c511b 100755 --- a/winsup/cygwin/scripts/speclib +++ b/winsup/cygwin/scripts/speclib @@ -38,6 +38,7 @@ while (<$nm_fd>) { study; if (/ I _?(.*)_dll_iname/o) { $dllname = $1; + $dllname =~ s/_2_0/-2.0/; } else { my ($file, $member, $symbol) = m%^([^:]*):([^:]*(?=:))?.* T (.*)%o; next if !defined($symbol) || $symbol =~ $exclude_regex; From 83eb4998d01f356dd132dc845cad20353d2198a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B8=CC=86=20=D0=9F?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=BE=D0=B2?= Date: Sun, 14 Apr 2019 21:09:17 +0300 Subject: [PATCH 032/102] Rename dll from cygwin to msys --- winsup/cygserver/transport_pipes.h | 4 +++ winsup/cygwin/Makefile.am | 27 ++++++++++--------- winsup/cygwin/crt0.c | 8 ++++++ winsup/cygwin/cygwin.din | 6 ++--- winsup/cygwin/cygwin.sc.in | 4 +++ winsup/cygwin/dcrt0.cc | 4 +++ winsup/cygwin/dlfcn.cc | 5 ++++ winsup/cygwin/dll_init.cc | 4 +++ winsup/cygwin/dtable.cc | 6 +++++ winsup/cygwin/exceptions.cc | 4 +-- winsup/cygwin/fhandler/pipe.cc | 4 +++ winsup/cygwin/fhandler/pty.cc | 26 ++++++++++++++++++ winsup/cygwin/hookapi.cc | 4 +++ winsup/cygwin/include/cygwin/cygwin_dll.h | 10 +++---- winsup/cygwin/include/cygwin/version.h | 8 ++++++ winsup/cygwin/lib/_cygwin_crt0_common.cc | 4 +++ winsup/cygwin/lib/crt0.h | 4 +++ winsup/cygwin/lib/cygwin_attach_dll.c | 8 ++++++ winsup/cygwin/lib/cygwin_crt0.c | 8 ++++++ .../cygwin/local_includes/cygserver_setpwd.h | 4 +++ winsup/cygwin/scripts/mkvers.sh | 6 ++--- winsup/cygwin/sec/auth.cc | 8 +++--- winsup/cygwin/syscalls.cc | 4 +-- winsup/cygwin/syslog.cc | 4 +++ winsup/cygwin/winver.rc | 2 +- winsup/testsuite/winsup.api/cygload.cc | 10 +++---- winsup/testsuite/winsup.api/cygload.h | 2 +- winsup/utils/ldd.cc | 2 +- winsup/utils/loadlib.h | 6 ++--- winsup/utils/mingw/cygcheck.cc | 27 +++++++++---------- winsup/utils/mingw/strace.cc | 9 +++---- winsup/utils/path.cc | 12 ++++----- winsup/utils/ssp.c | 8 +++--- 33 files changed, 180 insertions(+), 72 deletions(-) diff --git a/winsup/cygserver/transport_pipes.h b/winsup/cygserver/transport_pipes.h index e101623d24..66272bc86c 100644 --- a/winsup/cygserver/transport_pipes.h +++ b/winsup/cygserver/transport_pipes.h @@ -11,7 +11,11 @@ details. */ #ifndef _TRANSPORT_PIPES_H #define _TRANSPORT_PIPES_H +#ifdef __MSYS__ +#define PIPE_NAME_PREFIX L"\\\\.\\pipe\\msys-" +#else #define PIPE_NAME_PREFIX L"\\\\.\\pipe\\cygwin-" +#endif #define PIPE_NAME_SUFFIX L"-lpc" /* Named pipes based transport, for security on NT */ diff --git a/winsup/cygwin/Makefile.am b/winsup/cygwin/Makefile.am index 383f4f34a9..981877d733 100644 --- a/winsup/cygwin/Makefile.am +++ b/winsup/cygwin/Makefile.am @@ -37,12 +37,12 @@ newlib_build=$(target_builddir)/newlib toollibdir=$(tooldir)/lib toolincludedir=$(tooldir)/include -# Parameters used in building the cygwin.dll. +# Parameters used in building the msys-2.0.dll. -DLL_NAME=cygwin1.dll -NEW_DLL_NAME=new-cygwin1.dll -DEF_FILE=cygwin.def -LIB_NAME=libcygwin.a +DLL_NAME=msys-2.0.dll +NEW_DLL_NAME=new-msys-2.0.dll +DEF_FILE=msys.def +LIB_NAME=libmsys-2.0.a # # sources @@ -589,16 +589,16 @@ LIBSERVER = $(cygserver_blddir)/libcygserver.a $(LIBSERVER): $(MAKE) -C $(cygserver_blddir) libcygserver.a -# We build as new-cygwin1.dll and rename at install time to overcome native +# We build as new-msys-2.0.dll and rename at install time to overcome native # rebuilding issues (we don't want the build tools to see a partially built -# cygwin1.dll and attempt to use it instead of the old one). +# msys-2.0.dll and attempt to use it instead of the old one). # linker script LDSCRIPT=cygwin.sc $(LDSCRIPT): $(LDSCRIPT).in $(AM_V_GEN)$(CC) -E - -P < $^ -o $@ -# cygwin dll +# msys-2.0 dll # Set PE and export table header timestamps to zero for reproducible builds. $(NEW_DLL_NAME): $(LDSCRIPT) libdll.a $(VERSION_OFILES) $(LIBSERVER)\ $(newlib_build)/libm.a $(newlib_build)/libc.a @@ -607,18 +607,18 @@ $(NEW_DLL_NAME): $(LDSCRIPT) libdll.a $(VERSION_OFILES) $(LIBSERVER)\ -Wl,--gc-sections -nostdlib -Wl,-T$(LDSCRIPT) \ -Wl,--dynamicbase -static \ $${SOURCE_DATE_EPOCH:+-Wl,--no-insert-timestamp} \ - -Wl,--heap=0 -Wl,--out-implib,cygdll.a -shared -o $@ \ + -Wl,--heap=0 -Wl,--out-implib,msysdll.a -shared -o $@ \ -e @DLL_ENTRY@ $(DEF_FILE) \ -Wl,-whole-archive libdll.a -Wl,-no-whole-archive \ $(VERSION_OFILES) \ $(LIBSERVER) \ $(newlib_build)/libm.a \ $(newlib_build)/libc.a \ - -lgcc -lkernel32 -lntdll -Wl,-Map,cygwin.map + -lgcc -lkernel32 -lntdll -Wl,-Map,msys.map @$(MKDIR_P) ${target_builddir}/winsup/testsuite/testinst/bin/ $(AM_V_at)$(INSTALL_PROGRAM) $(NEW_DLL_NAME) ${target_builddir}/winsup/testsuite/testinst/bin/$(DLL_NAME) -# cygwin import library +# msys-2.0 import library toolopts=--cpu=@target_cpu@ --ar=@AR@ --as=@AS@ --nm=@NM@ --objcopy=@OBJCOPY@ $(DEF_FILE): scripts/gendef cygwin.din @@ -631,13 +631,14 @@ sigfe.s: $(DEF_FILE) tlsoffsets LIBCOS=$(addsuffix .o,$(basename $(LIB_FILES))) $(LIB_NAME): $(DEF_FILE) $(LIBCOS) | $(NEW_DLL_NAME) - $(AM_V_GEN)$(srcdir)/scripts/mkimport $(toolopts) $(NEW_FUNCTIONS) $@ cygdll.a $(wordlist 2,99,$^) + $(AM_V_GEN)$(srcdir)/scripts/mkimport $(toolopts) $(NEW_FUNCTIONS) $@ msysdll.a $(wordlist 2,99,$^) # sublibs # import libraries for some subset of symbols indicated by given objects speclib=\ $(srcdir)/scripts/speclib $(toolopts) \ --exclude='cygwin' \ + --exclude='msys' \ --exclude='(?i:dll)' \ --exclude='reloc' \ --exclude='^main$$' \ @@ -687,7 +688,7 @@ all-local: $(LIB_NAME) $(SUBLIBS) clean-local: -rm -f $(BUILT_SOURCES) -rm -f $(DEF_FILE) sigfe.s - -rm -f cygwin.sc cygdll.a cygwin.map + -rm -f cygwin.sc msysdll.a msys.map -rm -f $(NEW_DLL_NAME) -rm -f $(LIB_NAME) $(SUBLIBS) -rm -f version.cc diff --git a/winsup/cygwin/crt0.c b/winsup/cygwin/crt0.c index 1096e58970..3160df4491 100644 --- a/winsup/cygwin/crt0.c +++ b/winsup/cygwin/crt0.c @@ -9,12 +9,20 @@ details. */ extern int main (int argc, char **argv); +#ifdef __MSYS__ +void msys_crt0 (int (*main) (int, char **)); +#else void cygwin_crt0 (int (*main) (int, char **)); +#endif void mainCRTStartup () { +#ifdef __MSYS__ + msys_crt0 (main); +#else cygwin_crt0 (main); +#endif /* These are never actually called. They are just here to force the inclusion of things like -lbinmode. */ diff --git a/winsup/cygwin/cygwin.din b/winsup/cygwin/cygwin.din index deac201c08..073b8d07f4 100644 --- a/winsup/cygwin/cygwin.din +++ b/winsup/cygwin/cygwin.din @@ -1,4 +1,4 @@ -LIBRARY "cygwin1.dll" BASE=0x180040000 +LIBRARY "msys-2.0.dll" BASE=0x180040000 EXPORTS # Exported variables @@ -404,8 +404,8 @@ cygwin_attach_handle_to_fd SIGFE cygwin_conv_path SIGFE cygwin_conv_path_list SIGFE cygwin_create_path SIGFE -cygwin_detach_dll SIGFE_MAYBE -cygwin_dll_init NOSIGFE +msys_detach_dll SIGFE_MAYBE +msys_dll_init NOSIGFE cygwin_internal NOSIGFE cygwin_logon_user SIGFE cygwin_posix_path_list_p NOSIGFE diff --git a/winsup/cygwin/cygwin.sc.in b/winsup/cygwin/cygwin.sc.in index 69526f5d8a..4dc5daed8d 100644 --- a/winsup/cygwin/cygwin.sc.in +++ b/winsup/cygwin/cygwin.sc.in @@ -1,6 +1,10 @@ #ifdef __x86_64__ OUTPUT_FORMAT(pei-x86-64) +# ifdef __MSYS__ +SEARCH_DIR("/usr/x86_64-pc-msys/lib/w32api"); SEARCH_DIR("=/usr/lib/w32api"); +# else SEARCH_DIR("/usr/x86_64-pc-cygwin/lib/w32api"); SEARCH_DIR("=/usr/lib/w32api"); +# endif #else #error unimplemented for this target #endif diff --git a/winsup/cygwin/dcrt0.cc b/winsup/cygwin/dcrt0.cc index f4c09befd6..e19b7d3904 100644 --- a/winsup/cygwin/dcrt0.cc +++ b/winsup/cygwin/dcrt0.cc @@ -1077,7 +1077,11 @@ dll_crt0 (per_process *uptr) See winsup/testsuite/cygload for an example of how to use cygwin1.dll from MSVC and non-cygwin MinGW applications. */ extern "C" void +#ifdef __MSYS__ +msys_dll_init () +#else cygwin_dll_init () +#endif { static int _fmode; diff --git a/winsup/cygwin/dlfcn.cc b/winsup/cygwin/dlfcn.cc index e06616d7fa..40d99ddeff 100644 --- a/winsup/cygwin/dlfcn.cc +++ b/winsup/cygwin/dlfcn.cc @@ -148,8 +148,13 @@ collect_basenames (pathfinder::basenamelist & basenames, /* If the basename starts with "lib", ... */ if (!strncmp (basename, "lib", 3)) { +#ifdef __MSYS__ + /* ... replace "lib" with "msys-", before ... */ + basenames.appendv ("msys-", 5, basename+3, baselen-3, ext, extlen, NULL); +#else /* ... replace "lib" with "cyg", before ... */ basenames.appendv ("cyg", 3, basename+3, baselen-3, ext, extlen, NULL); +#endif } /* ... using original basename with new suffix. */ basenames.appendv (basename, baselen, ext, extlen, NULL); diff --git a/winsup/cygwin/dll_init.cc b/winsup/cygwin/dll_init.cc index 6fae8f1456..4a7c338cf1 100644 --- a/winsup/cygwin/dll_init.cc +++ b/winsup/cygwin/dll_init.cc @@ -913,7 +913,11 @@ dll_dllcrt0_1 (VOID *x) } extern "C" void +#ifdef __MSYS__ +msys_detach_dll (dll *) +#else cygwin_detach_dll (dll *) +#endif { HANDLE retaddr; if (_my_tls.isinitialized ()) diff --git a/winsup/cygwin/dtable.cc b/winsup/cygwin/dtable.cc index 7303f7eacc..6ccc19a715 100644 --- a/winsup/cygwin/dtable.cc +++ b/winsup/cygwin/dtable.cc @@ -997,9 +997,15 @@ handle_to_fn (HANDLE h, char *posix_fn) if (wcsncasecmp (w32, DEV_NAMED_PIPE, DEV_NAMED_PIPE_LEN) == 0) { w32 += DEV_NAMED_PIPE_LEN; +#ifdef __MSYS__ + if (wcsncmp (w32, L"msys-", WCLEN (L"msys-")) != 0) + return false; + w32 += WCLEN (L"msys-"); +#else if (wcsncmp (w32, L"cygwin-", WCLEN (L"cygwin-")) != 0) return false; w32 += WCLEN (L"cygwin-"); +#endif /* Check for installation key and trailing dash. */ w32len = cygheap->installation_key.Length / sizeof (WCHAR); if (w32len diff --git a/winsup/cygwin/exceptions.cc b/winsup/cygwin/exceptions.cc index 138ef5a749..3b7f23bf60 100644 --- a/winsup/cygwin/exceptions.cc +++ b/winsup/cygwin/exceptions.cc @@ -528,14 +528,14 @@ int exec_prepared_command (PWCHAR command) PWCHAR rawenv = GetEnvironmentStringsW () ; for (PWCHAR p = rawenv; *p != L'\0'; p = wcschr (p, L'\0') + 1) { - if (wcsncmp (p, L"CYGWIN=", wcslen (L"CYGWIN=")) == 0) + if (wcsncmp (p, L"MSYS=", wcslen (L"MSYS=")) == 0) { PWCHAR q = wcsstr (p, L"error_start") ; /* replace 'error_start=...' with '_rror_start=...' */ if (q) { *q = L'_' ; - SetEnvironmentVariableW (L"CYGWIN", p + wcslen (L"CYGWIN=")) ; + SetEnvironmentVariableW (L"MSYS", p + wcslen (L"MSYS=")) ; } break; } diff --git a/winsup/cygwin/fhandler/pipe.cc b/winsup/cygwin/fhandler/pipe.cc index 2ff5dfa2f7..11ef78c73b 100644 --- a/winsup/cygwin/fhandler/pipe.cc +++ b/winsup/cygwin/fhandler/pipe.cc @@ -798,7 +798,11 @@ fhandler_pipe::close (int flag) return ret; } +#ifdef __MSYS__ +#define PIPE_INTRO "\\\\.\\pipe\\msys-" +#else #define PIPE_INTRO "\\\\.\\pipe\\cygwin-" +#endif /* Create a pipe, and return handles to the read and write ends, just like CreatePipe, but ensure that the write end permits diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index c5081bcaf3..56cf6248f8 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -902,7 +902,11 @@ fhandler_pty_slave::open (int flags, mode_t) pipe_reply repl; DWORD len; +#ifdef __MSYS__ + __small_sprintf (buf, "\\\\.\\pipe\\msys-%S-pty%d-master-ctl", +#else __small_sprintf (buf, "\\\\.\\pipe\\cygwin-%S-pty%d-master-ctl", +#endif &cygheap->installation_key, get_minor ()); termios_printf ("dup handles via master control pipe %s", buf); if (!CallNamedPipe (buf, &req, sizeof req, &repl, sizeof repl, @@ -1170,7 +1174,11 @@ fhandler_pty_slave::reset_switch_to_nat_pipe (void) { char pipe[MAX_PATH]; __small_sprintf (pipe, +#ifdef __MSYS__ + "\\\\.\\pipe\\msys-%S-pty%d-master-ctl", +#else "\\\\.\\pipe\\cygwin-%S-pty%d-master-ctl", +#endif &cygheap->installation_key, get_minor ()); pipe_request req = { GET_HANDLES, GetCurrentProcessId () }; pipe_reply repl; @@ -1650,9 +1658,15 @@ fhandler_pty_slave::tcflush (int queue) if (queue == TCIFLUSH || queue == TCIOFLUSH) { char pipe[MAX_PATH]; +#ifdef __MSYS__ + __small_sprintf (pipe, + "\\\\.\\pipe\\msys-%S-pty%d-master-ctl", + &cygheap->installation_key, get_minor ()); +#else __small_sprintf (pipe, "\\\\.\\pipe\\cygwin-%S-pty%d-master-ctl", &cygheap->installation_key, get_minor ()); +#endif pipe_request req = { FLUSH_INPUT, GetCurrentProcessId () }; pipe_reply repl; DWORD n; @@ -2108,7 +2122,11 @@ fhandler_pty_master::close (int flag) pipe_reply repl; DWORD len; +#ifdef __MSYS__ + __small_sprintf (buf, "\\\\.\\pipe\\msys-%S-pty%d-master-ctl", +#else __small_sprintf (buf, "\\\\.\\pipe\\cygwin-%S-pty%d-master-ctl", +#endif &cygheap->installation_key, get_minor ()); acquire_output_mutex (mutex_timeout); if (master_ctl) @@ -3261,7 +3279,11 @@ fhandler_pty_master::setup () /* Create master control pipe which allows the master to duplicate the pty pipe handles to processes which deserve it. */ +#ifdef __MSYS__ + __small_sprintf (buf, "\\\\.\\pipe\\msys-%S-pty%d-master-ctl", +#else __small_sprintf (buf, "\\\\.\\pipe\\cygwin-%S-pty%d-master-ctl", +#endif &cygheap->installation_key, unit); master_ctl = CreateNamedPipe (buf, PIPE_ACCESS_DUPLEX | FILE_FLAG_FIRST_PIPE_INSTANCE, @@ -4154,7 +4176,11 @@ fhandler_pty_slave::transfer_input (tty::xfer_dir dir, HANDLE from, tty *ttyp, { char pipe[MAX_PATH]; __small_sprintf (pipe, +#ifdef __MSYS__ + "\\\\.\\pipe\\msys-%S-pty%d-master-ctl", +#else "\\\\.\\pipe\\cygwin-%S-pty%d-master-ctl", +#endif &cygheap->installation_key, ttyp->get_minor ()); pipe_request req = { GET_HANDLES, GetCurrentProcessId () }; pipe_reply repl; diff --git a/winsup/cygwin/hookapi.cc b/winsup/cygwin/hookapi.cc index ee2edbafee..9f31a716c4 100644 --- a/winsup/cygwin/hookapi.cc +++ b/winsup/cygwin/hookapi.cc @@ -379,7 +379,11 @@ hook_or_detect_cygwin (const char *name, const void *fn, WORD& subsys, HANDLE h) for (PIMAGE_IMPORT_DESCRIPTOR pd = pdfirst; pd->FirstThunk; pd++) { if (!ascii_strcasematch (rva (PSTR, map ?: (char *) hm, pd->Name - delta), +#ifdef __MSYS__ + "msys-2.0.dll")) +#else "cygwin1.dll")) +#endif continue; if (!fn) { diff --git a/winsup/cygwin/include/cygwin/cygwin_dll.h b/winsup/cygwin/include/cygwin/cygwin_dll.h index 1e4cf98ba5..b77598bb63 100644 --- a/winsup/cygwin/include/cygwin/cygwin_dll.h +++ b/winsup/cygwin/include/cygwin/cygwin_dll.h @@ -24,8 +24,8 @@ details. */ CDECL_BEGIN \ int Entry (HINSTANCE h, DWORD reason, void *ptr); \ typedef int (*mainfunc) (int, char **, char **); \ - extern PVOID cygwin_attach_dll (HMODULE, mainfunc); \ - extern void cygwin_detach_dll (PVOID); \ + extern PVOID msys_attach_dll (HMODULE, mainfunc); \ + extern void msys_detach_dll (PVOID); \ CDECL_END \ \ static HINSTANCE storedHandle; \ @@ -42,7 +42,7 @@ static int __dllMain (int a __attribute__ ((__unused__)), \ \ static PVOID dll_index; \ \ -int _cygwin_dll_entry (HINSTANCE h, DWORD reason, void *ptr) \ +int _msys_dll_entry (HINSTANCE h, DWORD reason, void *ptr) \ { \ int ret; \ ret = 1; \ @@ -55,7 +55,7 @@ int _cygwin_dll_entry (HINSTANCE h, DWORD reason, void *ptr) \ storedReason = reason; \ storedPtr = ptr; \ __dynamically_loaded = (ptr == NULL); \ - dll_index = cygwin_attach_dll (h, &__dllMain); \ + dll_index = msys_attach_dll (h, &__dllMain); \ if (dll_index == (PVOID) -1) \ ret = 0; \ } \ @@ -66,7 +66,7 @@ int _cygwin_dll_entry (HINSTANCE h, DWORD reason, void *ptr) \ ret = Entry (h, reason, ptr); \ if (ret) \ { \ - cygwin_detach_dll (dll_index); \ + msys_detach_dll (dll_index); \ dll_index = (PVOID) -1; \ } \ } \ diff --git a/winsup/cygwin/include/cygwin/version.h b/winsup/cygwin/include/cygwin/version.h index 8c454e4485..72aa76de1d 100644 --- a/winsup/cygwin/include/cygwin/version.h +++ b/winsup/cygwin/include/cygwin/version.h @@ -510,7 +510,11 @@ details. */ names include the CYGWIN_VERSION_SHARED_DATA version as well as this identifier. */ +#ifdef __MSYS__ +#define CYGWIN_VERSION_DLL_IDENTIFIER "msys-2.0" +#else #define CYGWIN_VERSION_DLL_IDENTIFIER "cygwin1" +#endif /* The Cygwin mount table interface in the Win32 registry also has a version number associated with it in case that is changed in a non-backwards @@ -526,7 +530,11 @@ details. */ /* Identifiers used in the Win32 registry. */ +#ifdef __MSYS__ +#define CYGWIN_INFO_CYGWIN_REGISTRY_NAME "MSYS" +#else #define CYGWIN_INFO_CYGWIN_REGISTRY_NAME "Cygwin" +#endif #define CYGWIN_INFO_INSTALLATIONS_NAME "Installations" /* The default cygdrive prefix. */ diff --git a/winsup/cygwin/lib/_cygwin_crt0_common.cc b/winsup/cygwin/lib/_cygwin_crt0_common.cc index d356a50fba..801b6f91ca 100644 --- a/winsup/cygwin/lib/_cygwin_crt0_common.cc +++ b/winsup/cygwin/lib/_cygwin_crt0_common.cc @@ -73,7 +73,11 @@ struct per_process_cxx_malloc __cygwin_cxx_malloc = and then jump to the dll. */ int +#ifdef __MSYS__ +_msys_crt0_common (MainFunc f, per_process *u) +#else _cygwin_crt0_common (MainFunc f, per_process *u) +#endif { per_process *newu = (per_process *) cygwin_internal (CW_USER_DATA); bool uwasnull; diff --git a/winsup/cygwin/lib/crt0.h b/winsup/cygwin/lib/crt0.h index e599b44934..e81750032b 100644 --- a/winsup/cygwin/lib/crt0.h +++ b/winsup/cygwin/lib/crt0.h @@ -13,7 +13,11 @@ extern "C" { #include "winlean.h" struct per_process; typedef int (*MainFunc) (int argc, char *argv[], char **env); +#ifdef __MSYS__ +int _msys_crt0_common (MainFunc, struct per_process *); +#else int _cygwin_crt0_common (MainFunc, struct per_process *); +#endif PVOID dll_dllcrt0 (HMODULE, struct per_process *); #ifdef __cplusplus diff --git a/winsup/cygwin/lib/cygwin_attach_dll.c b/winsup/cygwin/lib/cygwin_attach_dll.c index 866bfd80fa..82679c4a97 100644 --- a/winsup/cygwin/lib/cygwin_attach_dll.c +++ b/winsup/cygwin/lib/cygwin_attach_dll.c @@ -15,10 +15,18 @@ details. */ /* for a loaded dll */ PVOID +#ifdef __MSYS__ +msys_attach_dll (HMODULE h, MainFunc f) +#else cygwin_attach_dll (HMODULE h, MainFunc f) +#endif { static struct per_process u; +#ifdef __MSYS__ + (void) _msys_crt0_common (f, &u); +#else (void) _cygwin_crt0_common (f, &u); +#endif /* jump into the dll. */ return dll_dllcrt0 (h, &u); diff --git a/winsup/cygwin/lib/cygwin_crt0.c b/winsup/cygwin/lib/cygwin_crt0.c index 7020a639dd..396447e52e 100644 --- a/winsup/cygwin/lib/cygwin_crt0.c +++ b/winsup/cygwin/lib/cygwin_crt0.c @@ -14,8 +14,16 @@ extern void _dll_crt0 () /* for main module */ void +#ifdef __MSYS__ +msys_crt0 (MainFunc f) +#else cygwin_crt0 (MainFunc f) +#endif { +#ifdef __MSYS__ + _msys_crt0_common (f, NULL); +#else _cygwin_crt0_common (f, NULL); +#endif _dll_crt0 (); /* Jump into the dll, never to return */ } diff --git a/winsup/cygwin/local_includes/cygserver_setpwd.h b/winsup/cygwin/local_includes/cygserver_setpwd.h index fc1576b059..b2975111cf 100644 --- a/winsup/cygwin/local_includes/cygserver_setpwd.h +++ b/winsup/cygwin/local_includes/cygserver_setpwd.h @@ -12,7 +12,11 @@ details. */ #include #include "cygserver.h" +#ifdef __MSYS__ +#define CYGWIN_LSA_KEY_PREFIX L"L$MSYS_" +#else #define CYGWIN_LSA_KEY_PREFIX L"L$CYGWIN_" +#endif #ifndef __INSIDE_CYGWIN__ class transport_layer_base; diff --git a/winsup/cygwin/scripts/mkvers.sh b/winsup/cygwin/scripts/mkvers.sh index 38f439cd0d..a3d45c5db0 100755 --- a/winsup/cygwin/scripts/mkvers.sh +++ b/winsup/cygwin/scripts/mkvers.sh @@ -123,7 +123,7 @@ dir=$(echo $dir | sed -e 's%/include/cygwin.*$%%' -e 's%include/cygwin.*$%.%') ) | while read var; do read val cat <&9 @@ -135,9 +135,9 @@ trap "rm -f /tmp/mkvers.$$" 0 1 2 15 # cat <&9 #ifdef DEBUGGING - "%%% Cygwin shared id: " CYGWIN_VERSION_DLL_IDENTIFIER "S" shared_data_version "-$builddate\n" + "%%% MSYS shared id: " CYGWIN_VERSION_DLL_IDENTIFIER "S" shared_data_version "-$builddate\n" #else - "%%% Cygwin shared id: " CYGWIN_VERSION_DLL_IDENTIFIER "S" shared_data_version "\n" + "%%% MSYS shared id: " CYGWIN_VERSION_DLL_IDENTIFIER "S" shared_data_version "\n" #endif "END_CYGWIN_VERSION_INFO\n\0"; cygwin_version_info cygwin_version = diff --git a/winsup/cygwin/sec/auth.cc b/winsup/cygwin/sec/auth.cc index f9906a55c6..2361ae5ff9 100644 --- a/winsup/cygwin/sec/auth.cc +++ b/winsup/cygwin/sec/auth.cc @@ -462,7 +462,7 @@ verify_token (HANDLE token, cygsid &usersid, user_groups &groups, bool *pintern) if (!NT_SUCCESS (status)) debug_printf ("NtQueryInformationToken(), %y", status); else - *pintern = intern = !memcmp (ts.SourceName, "Cygwin.1", 8); + *pintern = intern = !memcmp (ts.SourceName, "MSYS.2", 6); } /* Verify usersid */ cygsid tok_usersid (NO_SID); @@ -747,7 +747,7 @@ s4uauth (bool logon, PCWSTR domain, PCWSTR user, NTSTATUS &ret_status) { /* Register as logon process. */ debug_printf ("Impersonation requested"); - RtlInitAnsiString (&name, "Cygwin"); + RtlInitAnsiString (&name, "MSYS"); status = LsaRegisterLogonProcess (&name, &lsa_hdl, &sec_mode); } else @@ -786,11 +786,11 @@ s4uauth (bool logon, PCWSTR domain, PCWSTR user, NTSTATUS &ret_status) } /* Create origin. */ - stpcpy (origin.buf, "Cygwin"); + stpcpy (origin.buf, "MSYS"); RtlInitAnsiString (&origin.str, origin.buf); /* Create token source. */ - memcpy (ts.SourceName, "Cygwin.1", 8); + memcpy (ts.SourceName, "MSYS.2", 6); ts.SourceIdentifier.HighPart = 0; ts.SourceIdentifier.LowPart = kerberos_auth ? 0x0105 : 0x0106; diff --git a/winsup/cygwin/syscalls.cc b/winsup/cygwin/syscalls.cc index 2bea797680..12b7a4f2fb 100644 --- a/winsup/cygwin/syscalls.cc +++ b/winsup/cygwin/syscalls.cc @@ -339,7 +339,7 @@ try_to_bin (path_conv &pc, HANDLE &fh, ACCESS_MASK access, ULONG flags) } else { - /* Create unique filename. Start with a dot, followed by "cyg" + /* Create unique filename. Start with a dot, followed by "msys" transposed to the Unicode private use area in the U+f700 area on file systems supporting Unicode (except Samba), followed by the inode number in hex, followed by a path hash in hex. The @@ -347,7 +347,7 @@ try_to_bin (path_conv &pc, HANDLE &fh, ACCESS_MASK access, ULONG flags) RtlAppendUnicodeToString (&recycler, (pc.fs_flags () & FILE_UNICODE_ON_DISK && !pc.fs_is_samba ()) - ? L".\xf763\xf779\xf767" : L".cyg"); + ? L".\xf76d\xf773\xf779\xf773" : L".msys"); pfii = (PFILE_INTERNAL_INFORMATION) infobuf; status = NtQueryInformationFile (fh, &io, pfii, sizeof *pfii, FileInternalInformation); diff --git a/winsup/cygwin/syslog.cc b/winsup/cygwin/syslog.cc index 6a295501f1..431f9d2396 100644 --- a/winsup/cygwin/syslog.cc +++ b/winsup/cygwin/syslog.cc @@ -26,7 +26,11 @@ details. */ #include "cygtls.h" #include "tls_pbuf.h" +#ifdef __MSYS__ +#define CYGWIN_LOG_NAME L"MSYS" +#else #define CYGWIN_LOG_NAME L"Cygwin" +#endif static struct { diff --git a/winsup/cygwin/winver.rc b/winsup/cygwin/winver.rc index 980d51204c..58878d41bd 100644 --- a/winsup/cygwin/winver.rc +++ b/winsup/cygwin/winver.rc @@ -35,7 +35,7 @@ BEGIN VALUE "InternalName", CYGWIN_DLL_NAME VALUE "LegalCopyright", "Copyright \251 Cygwin Authors 1996-" STRINGIFY(CYGWIN_BUILD_YEAR) VALUE "OriginalFilename", CYGWIN_DLL_NAME - VALUE "ProductName", "Cygwin" + VALUE "ProductName", "MSYS2" VALUE "ProductVersion", STRINGIFY(CYGWIN_VERSION) VALUE "APIVersion", CYGWIN_API_VERSION VALUE "SharedMemoryVersion", STRINGIFY(CYGWIN_VERSION_SHARED_DATA) diff --git a/winsup/testsuite/winsup.api/cygload.cc b/winsup/testsuite/winsup.api/cygload.cc index afd3ee90fc..1b2f79dc05 100644 --- a/winsup/testsuite/winsup.api/cygload.cc +++ b/winsup/testsuite/winsup.api/cygload.cc @@ -25,7 +25,7 @@ save for errors. -testinterrupts Pauses the program for 30 seconds so you can demonstrate that it handles ^C properly. - -cygwin Name of DLL to load. Defaults to "cygwin1.dll". */ + -cygwin Name of DLL to load. Defaults to "msys-2.0.dll". */ #include "cygload.h" #include @@ -154,13 +154,13 @@ cygwin::connector::connector (const char *dll) *out << "Initializing cygwin..." << endl; - // This calls dcrt0.cc:cygwin_dll_init(), which calls dll_crt0_1(), + // This calls dcrt0.cc:msys_dll_init(), which calls dll_crt0_1(), // which will, among other things: // * spawn the cygwin signal handling thread from sigproc_init() // * initialize the thread-local storage for this thread and overwrite // the first 4K of the stack void (*cyginit) (); - get_symbol ("cygwin_dll_init", cyginit); + get_symbol ("msys_dll_init", cyginit); (*cyginit) (); *out << "Loading symbols..." << endl; @@ -224,7 +224,7 @@ cygwin::connector::~connector () // This should call init.cc:dll_entry() with DLL_PROCESS_DETACH. if (!FreeLibrary (_library)) - throw windows_error ("FreeLibrary", "cygwin1.dll"); + throw windows_error ("FreeLibrary", "msys-2.0.dll"); } catch (std::exception &x) { @@ -490,7 +490,7 @@ main (int argc, char *argv[]) std::ostringstream output; bool verbose = false, testinterrupts = false; - const char *dll = "cygwin1.dll"; + const char *dll = "msys-2.0.dll"; out = &output; diff --git a/winsup/testsuite/winsup.api/cygload.h b/winsup/testsuite/winsup.api/cygload.h index 30154048b0..0f2aacda9a 100644 --- a/winsup/testsuite/winsup.api/cygload.h +++ b/winsup/testsuite/winsup.api/cygload.h @@ -76,7 +76,7 @@ namespace cygwin // spawns a thread to let you receive signals from cygwin. class connector { public: - connector (const char *dll = "cygwin1.dll"); + connector (const char *dll = "msys-2.0.dll"); ~connector (); // A wrapper around GetProcAddress() for fetching symbols from the diff --git a/winsup/utils/ldd.cc b/winsup/utils/ldd.cc index 0d073c2989..a31c4c6e44 100644 --- a/winsup/utils/ldd.cc +++ b/winsup/utils/ldd.cc @@ -249,7 +249,7 @@ tocyg (wchar_t *win_fn) return fn; } -#define CYGWIN_DLL_LEN (wcslen (L"\\cygwin1.dll")) +#define CYGWIN_DLL_LEN (wcslen (L"\\msys-2.0.dll")) static int print_dlls (dlls *dll, const wchar_t *dllfn, const wchar_t *process_fn) { diff --git a/winsup/utils/loadlib.h b/winsup/utils/loadlib.h index c83b76478f..42ffbfdc03 100644 --- a/winsup/utils/loadlib.h +++ b/winsup/utils/loadlib.h @@ -13,7 +13,7 @@ #include /* Load all system libs from the windows system directory by prepending the - full path. This doesn't work for loadling cygwin1.dll. For this case, + full path. This doesn't work for loadling msys-2.0.dll. For this case, instead of prepending the path, make sure that the CWD is removed from the DLL search path, if possible (XP SP1++, Vista++). */ static HMODULE _load_sys_library (const wchar_t *dll) __attribute__ ((used)); @@ -45,8 +45,8 @@ _load_sys_library (const wchar_t *dll) set_dll_directory (L""); } - if (wcscmp (dll, L"cygwin1.dll") == 0) - return LoadLibraryExW (L"cygwin1.dll", NULL, LOAD_WITH_ALTERED_SEARCH_PATH); + if (wcscmp (dll, L"msys-2.0.dll") == 0) + return LoadLibraryExW (L"msys-2.0.dll", NULL, LOAD_WITH_ALTERED_SEARCH_PATH); wcscpy (dllpath, sysdir); wcscpy (dllpath + sysdir_len, dll); diff --git a/winsup/utils/mingw/cygcheck.cc b/winsup/utils/mingw/cygcheck.cc index 89a08e560f..1637683c26 100644 --- a/winsup/utils/mingw/cygcheck.cc +++ b/winsup/utils/mingw/cygcheck.cc @@ -95,8 +95,7 @@ static const char *known_env_vars[] = { "c_include_path", "compiler_path", "cxx_include_path", - "cygwin", - "cygwin32", + "msys", "dejagnu", "expect", "gcc_default_options", @@ -554,7 +553,7 @@ struct ImpDirectory static bool track_down (const char *file, const char *suffix, int lvl); -#define CYGPREFIX (sizeof ("%%% Cygwin ") - 1) +#define CYGPREFIX (sizeof ("%%% Msys ") - 1) static void cygwin_info (HANDLE h) { @@ -586,7 +585,7 @@ cygwin_info (HANDLE h) while (buf < bufend) if ((buf = (char *) memchr (buf, '%', bufend - buf)) == NULL) break; - else if (strncmp ("%%% Cygwin ", buf, CYGPREFIX) != 0) + else if (strncmp ("%%% Msys ", buf, CYGPREFIX) != 0) buf++; else { @@ -780,7 +779,7 @@ dll_info (const char *path, HANDLE fh, int lvl, int recurse) } } } - if (strstr (path, "\\cygwin1.dll")) + if (strstr (path, "\\msys-2.0.dll")) cygwin_info (fh); } @@ -1027,7 +1026,7 @@ scan_registry (RegInfo * prev, HKEY hKey, char *name, int cygwin, bool wow64) char *cp; for (cp = name; *cp; cp++) - if (strncasecmp (cp, "Cygwin", 6) == 0) + if (strncasecmp (cp, "Msys", 4) == 0) cygwin = 1; DWORD num_subkeys, max_subkey_len, num_values; @@ -1309,7 +1308,7 @@ handle_reg_installation (handle_reg_t what) printf ("Cygwin installations found in the registry:\n"); for (int i = 0; i < 2; ++i) if (RegOpenKeyEx (i ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE, - "SOFTWARE\\Cygwin\\Installations", 0, + "SOFTWARE\\Msys\\Installations", 0, what == DELETE_KEY ? KEY_READ | KEY_WRITE : KEY_READ, &key) == ERROR_SUCCESS) @@ -1331,7 +1330,7 @@ handle_reg_installation (handle_reg_t what) if (what == PRINT_KEY) printf (" %s Key: %s Path: %s", i ? "User: " : "System:", name, path); - strcat (path, "\\bin\\cygwin1.dll"); + strcat (path, "\\bin\\msys-2.0.dll"); if (what == PRINT_KEY) printf ("%s\n", access (path, F_OK) ? " (ORPHANED)" : ""); else if (access (path, F_OK)) @@ -1785,7 +1784,7 @@ dump_sysinfo () if (registry) { if (givehelp) - printf ("Scanning registry for keys with 'Cygwin' in them...\n"); + printf ("Scanning registry for keys with 'Msys' in them...\n"); scan_registry (0, HKEY_CURRENT_USER, (char *) "HKEY_CURRENT_USER", 0, false); scan_registry (0, HKEY_LOCAL_MACHINE, @@ -1980,10 +1979,10 @@ dump_sysinfo () wcstombs (f, ffinfo.cFileName, sizeof f); if (strcasecmp (f + strlen (f) - 4, ".dll") == 0) { - if (strncasecmp (f, "cyg", 3) == 0) + if (strncasecmp (f, "msys-", 5) == 0) { sprintf (tmp, "%s%s", pth->dir, f); - if (strcasecmp (f, "cygwin1.dll") == 0) + if (strcasecmp (f, "msys-2.0.dll") == 0) { if (!cygwin_dll_count) strcpy (cygdll_path, pth->dir); @@ -2007,9 +2006,9 @@ dump_sysinfo () FindClose (ff); } if (cygwin_dll_count > 1) - puts ("Warning: There are multiple cygwin1.dlls on your path"); + puts ("Warning: There are multiple msys-2.0.dlls on your path"); if (!cygwin_dll_count) - puts ("Warning: cygwin1.dll not found on your path"); + puts ("Warning: msys-2.0.dll not found on your path"); dump_dodgy_apps (verbose); @@ -3023,7 +3022,7 @@ load_cygwin (int& argc, char **&argv) { HMODULE h; - if (!(h = LoadLibrary ("cygwin1.dll"))) + if (!(h = LoadLibrary ("msys-2.0.dll"))) return; GetModuleFileNameW (h, cygwin_dll_path, 32768); if ((cygwin_internal = (uintptr_t (*) (cygwin_getinfo_types, ...)) diff --git a/winsup/utils/mingw/strace.cc b/winsup/utils/mingw/strace.cc index c220643b33..29db640239 100644 --- a/winsup/utils/mingw/strace.cc +++ b/winsup/utils/mingw/strace.cc @@ -284,7 +284,7 @@ load_cygwin () if (h) return 0; - if (!(h = LoadLibrary ("cygwin1.dll"))) + if (!(h = LoadLibrary ("msys-2.0.dll"))) { errno = ENOENT; return 0; @@ -354,17 +354,16 @@ create_child (char **argv) make_command_line (one_line, argv); SetConsoleCtrlHandler (NULL, 0); - - const char *cygwin_env = getenv ("CYGWIN"); + const char *cygwin_env = getenv ("MSYS"); const char *space; if (cygwin_env && strlen (cygwin_env) <= 256) /* sanity check */ space = " "; else space = cygwin_env = ""; - char *newenv = (char *) malloc (sizeof ("CYGWIN=noglob") + char *newenv = (char *) malloc (sizeof ("MSYS=noglob") + strlen (space) + strlen (cygwin_env)); - sprintf (newenv, "CYGWIN=noglob%s%s", space, cygwin_env); + sprintf (newenv, "MSYS=noglob%s%s", space, cygwin_env); _putenv (newenv); ret = CreateProcess (0, one_line.buf, /* command line */ NULL, /* Security */ diff --git a/winsup/utils/path.cc b/winsup/utils/path.cc index fe55a646d9..323e4c784b 100644 --- a/winsup/utils/path.cc +++ b/winsup/utils/path.cc @@ -585,14 +585,14 @@ read_mounts () } max_mount_entry = 0; - /* First fetch the cygwin1.dll path from the LoadLibrary call in load_cygwin. - This utilizes the DLL search order to find a matching cygwin1.dll and to + /* First fetch the msys-2.0.dll path from the LoadLibrary call in load_cygwin. + This utilizes the DLL search order to find a matching msys-2.0.dll and to compute the installation path from that DLL's path. */ if (cygwin_dll_path[0]) wcscpy (path, cygwin_dll_path); - /* If we can't load cygwin1.dll, check where cygcheck is living itself and - try to fetch installation path from here. Does cygwin1.dll exist in the - same path? This should only kick in if the cygwin1.dll in the same path + /* If we can't load msys-2.0.dll, check where cygcheck is living itself and + try to fetch installation path from here. Does msys-2.0.dll exist in the + same path? This should only kick in if the msys-2.0.dll in the same path has been made non-executable for the current user accidentally. */ else if (!GetModuleFileNameW (NULL, path, 32768)) return; @@ -601,7 +601,7 @@ read_mounts () { if (!cygwin_dll_path[0]) { - wcscpy (path_end, L"\\cygwin1.dll"); + wcscpy (path_end, L"\\msys-2.0.dll"); DWORD attr = GetFileAttributesW (path); if (attr == (DWORD) -1 || (attr & (FILE_ATTRIBUTE_DIRECTORY diff --git a/winsup/utils/ssp.c b/winsup/utils/ssp.c index 96a90a1d98..95045e1e8b 100644 --- a/winsup/utils/ssp.c +++ b/winsup/utils/ssp.c @@ -710,15 +710,15 @@ usage (FILE * stream) "You must specify the range of memory addresses to keep track of\n" "manually, but it's not hard to figure out what to specify. Use the\n" "\"objdump\" program to determine the bounds of the target's \".text\"\n" - "section. Let's say we're profiling cygwin1.dll. Make sure you've\n" + "section. Let's say we're profiling msys-2.0.dll. Make sure you've\n" "built it with debug symbols (else gprof won't run) and run objdump\n" "like this:\n" "\n" - " objdump -h cygwin1.dll\n" + " objdump -h msys-2.0.dll\n" "\n" "It will print a report like this:\n" "\n" - "cygwin1.dll: file format pei-i386\n" + "msys-2.0.dll: file format pei-i386\n" "\n" "Sections:\n" "Idx Name Size VMA LMA File off Algn\n" @@ -749,7 +749,7 @@ usage (FILE * stream) "\"gmon.out\". You can turn this data file into a readable report with\n" "gprof:\n" "\n" - " gprof -b cygwin1.dll\n" + " gprof -b msys-2.0.dll\n" "\n" "The \"-b\" means 'skip the help pages'. You can omit this until you're\n" "familiar with the report layout. The gprof documentation explains\n" From 612c696a2c6185a4cdde2c494ab0cb75fc78c057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B8=CC=86=20=D0=9F?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=BE=D0=B2?= Date: Sun, 14 Apr 2019 21:17:46 +0300 Subject: [PATCH 033/102] Convert Unix paths in args/env to Windows form for native Win32 apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Many of our build processes are made up of a mix of Cygwin tools (makepkg/bash for starters) and native Windows tools. When building things the paths of input and output files and directories are often communicated between them via process arguments or environment variables. The problem here is that those are in many cases not compatible. This introduces some "magic" where passing Unix paths to Win32 applications (via command-line arguments and/or environment variables) are auto-converted to their Win32 variants. Sometimes, this behavior is undesirable (e.g. when passing regular expressions via the form that starts and ends in slashes, which this new logic can mistake for Unix paths). For these instances, there are two ways to disable the automatic path conversion: - by setting `MSYS_NO_PATHCONV` to a non-empty string, - surgically, by setting the `MSYS2_ENV_CONV_EXCL` and `MSYS2_ARG_CONV_EXCL` environment variables. Find a more verbose description at https://www.msys2.org/docs/filesystem-paths/. Co-authored-by: Christoph Reiter Co-authored-by: 마누엘 Co-authored-by: Johannes Schindelin --- winsup/cygwin/Makefile.am | 1 + winsup/cygwin/environ.cc | 24 +- winsup/cygwin/external.cc | 2 +- winsup/cygwin/include/sys/cygwin.h | 6 + winsup/cygwin/local_includes/environ.h | 2 +- winsup/cygwin/local_includes/winf.h | 4 + winsup/cygwin/msys2_path_conv.cc | 709 +++++++++++++++++++++++++ winsup/cygwin/msys2_path_conv.h | 147 +++++ winsup/cygwin/path.cc | 69 +++ winsup/cygwin/spawn.cc | 38 +- 10 files changed, 998 insertions(+), 4 deletions(-) create mode 100644 winsup/cygwin/msys2_path_conv.cc create mode 100644 winsup/cygwin/msys2_path_conv.h diff --git a/winsup/cygwin/Makefile.am b/winsup/cygwin/Makefile.am index 981877d733..54ae637450 100644 --- a/winsup/cygwin/Makefile.am +++ b/winsup/cygwin/Makefile.am @@ -314,6 +314,7 @@ DLL_FILES= \ miscfuncs.cc \ mktemp.cc \ msg.cc \ + msys2_path_conv.cc \ mount.cc \ net.cc \ netdb.cc \ diff --git a/winsup/cygwin/environ.cc b/winsup/cygwin/environ.cc index d4cedcbdfe..639e69393b 100644 --- a/winsup/cygwin/environ.cc +++ b/winsup/cygwin/environ.cc @@ -1046,7 +1046,7 @@ env_compare (const void *key, const void *memb) to the child. */ char ** build_env (const char * const *envp, PWCHAR &envblock, int &envc, - bool no_envblock, HANDLE new_token) + bool no_envblock, HANDLE new_token, bool keep_posix) { PWCHAR cwinenv = NULL; size_t winnum = 0; @@ -1139,6 +1139,19 @@ build_env (const char * const *envp, PWCHAR &envblock, int &envc, for (srcp = envp, dstp = newenv, pass_dstp = pass_env; *srcp; srcp++) { bool calc_tl = !no_envblock; +#ifdef __MSYS__ + /* Don't pass timezone environment to non-msys applications */ + if (!keep_posix && ascii_strncasematch(*srcp, "TZ=", 3)) + { + const char *v = *srcp + 3; + if (*v == ':') + goto next1; + for (; *v; v++) + if (!isalpha(*v) && !isdigit(*v) && + *v != '-' && *v != '+' && *v != ':') + goto next1; + } +#endif /* Look for entries that require special attention */ for (unsigned i = 0; i < SPENVS_SIZE; i++) if (!saw_spenv[i] && (*dstp = spenvs[i].retrieve (no_envblock, *srcp))) @@ -1259,6 +1272,15 @@ build_env (const char * const *envp, PWCHAR &envblock, int &envc, saw_PATH = true; } } +#ifdef __MSYS__ + else if (!keep_posix) { + char *win_arg = arg_heuristic(*srcp); + debug_printf("WIN32_PATH is %s", win_arg); + p = cstrdup1(win_arg); + if (win_arg != *srcp) + free (win_arg); + } +#endif else p = *srcp; /* Don't worry about it */ diff --git a/winsup/cygwin/external.cc b/winsup/cygwin/external.cc index 50a5af24f9..a20ea078e3 100644 --- a/winsup/cygwin/external.cc +++ b/winsup/cygwin/external.cc @@ -141,7 +141,7 @@ create_winenv (const char * const *env) int unused_envc; PWCHAR envblock = NULL; char **envp = build_env (env ?: environ, envblock, unused_envc, false, - NULL); + NULL, true); PWCHAR p = envblock; if (envp) diff --git a/winsup/cygwin/include/sys/cygwin.h b/winsup/cygwin/include/sys/cygwin.h index f5c90fe96b..0e11a9b81a 100644 --- a/winsup/cygwin/include/sys/cygwin.h +++ b/winsup/cygwin/include/sys/cygwin.h @@ -60,6 +60,12 @@ extern ssize_t cygwin_conv_path_list (cygwin_conv_path_t what, const void *from, to one of the above values, or to ENOMEM if malloc fails. */ extern void *cygwin_create_path (cygwin_conv_path_t what, const void *from); +extern char * arg_heuristic_with_exclusions (char const * const arg, + char const * exclusions, + size_t exclusions_count); + +extern char * arg_heuristic (char const * const); + extern pid_t cygwin_winpid_to_pid (int); extern int cygwin_posix_path_list_p (const char *); extern void cygwin_split_path (const char *, char *, char *); diff --git a/winsup/cygwin/local_includes/environ.h b/winsup/cygwin/local_includes/environ.h index 86e64a72f9..0dd45359cc 100644 --- a/winsup/cygwin/local_includes/environ.h +++ b/winsup/cygwin/local_includes/environ.h @@ -34,7 +34,7 @@ win_env *getwinenv (const char *name, const char *posix = NULL, win_env * = NULL char *getwinenveq (const char *name, size_t len, int); char **build_env (const char * const *envp, PWCHAR &envblock, - int &envc, bool need_envblock, HANDLE new_token); + int &envc, bool need_envblock, HANDLE new_token, bool keep_posix); char **win32env_to_cygenv (PWCHAR rawenv, bool posify); diff --git a/winsup/cygwin/local_includes/winf.h b/winsup/cygwin/local_includes/winf.h index b586934410..bc53cd1aa3 100644 --- a/winsup/cygwin/local_includes/winf.h +++ b/winsup/cygwin/local_includes/winf.h @@ -56,6 +56,10 @@ class av calloced = 1; } } + void replace (int i, const char *arg) + { + argv[i] = cstrdup1 (arg); + } void dup_all () { for (int i = calloced; i < argc; i++) diff --git a/winsup/cygwin/msys2_path_conv.cc b/winsup/cygwin/msys2_path_conv.cc new file mode 100644 index 0000000000..4c0cc82cf2 --- /dev/null +++ b/winsup/cygwin/msys2_path_conv.cc @@ -0,0 +1,709 @@ +/* + The MSYS2 Path conversion source code is licensed under: + + CC0 1.0 Universal + + Official translations of this legal tool are available + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + + Statement of Purpose + + The laws of most jurisdictions throughout the world automatically + confer exclusive Copyright and Related Rights (defined below) upon the + creator and subsequent owner(s) (each and all, an "owner") of an + original work of authorship and/or a database (each, a "Work"). + + Certain owners wish to permanently relinquish those rights to a Work + for the purpose of contributing to a commons of creative, cultural and + scientific works ("Commons") that the public can reliably and without + fear of later claims of infringement build upon, modify, incorporate + in other works, reuse and redistribute as freely as possible in any + form whatsoever and for any purposes, including without limitation + commercial purposes. These owners may contribute to the Commons to + promote the ideal of a free culture and the further production of + creative, cultural and scientific works, or to gain reputation or + greater distribution for their Work in part through the use and + efforts of others. + + For these and/or other purposes and motivations, and without any + expectation of additional consideration or compensation, the person + associating CC0 with a Work (the "Affirmer"), to the extent that he or + she is an owner of Copyright and Related Rights in the Work, + voluntarily elects to apply CC0 to the Work and publicly distribute + the Work under its terms, with knowledge of his or her Copyright and + Related Rights in the Work and the meaning and intended legal effect + of CC0 on those rights. + + 1. Copyright and Related Rights. A Work made available under CC0 may + be protected by copyright and related or neighboring rights + ("Copyright and Related Rights"). Copyright and Related Rights + include, but are not limited to, the following: + + the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + moral rights retained by the original author(s) and/or performer(s); + publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + rights protecting the extraction, dissemination, use and reuse of data + in a Work; + database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and + other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations + thereof. + + 2. Waiver. To the greatest extent permitted by, but not in + contravention of, applicable law, Affirmer hereby overtly, fully, + permanently, irrevocably and unconditionally waives, abandons, and + surrenders all of Affirmer's Copyright and Related Rights and + associated claims and causes of action, whether now known or unknown + (including existing as well as future claims and causes of action), in + the Work (i) in all territories worldwide, (ii) for the maximum + duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number + of copies, and (iv) for any purpose whatsoever, including without + limitation commercial, advertising or promotional purposes (the + "Waiver"). Affirmer makes the Waiver for the benefit of each member of + the public at large and to the detriment of Affirmer's heirs and + successors, fully intending that such Waiver shall not be subject to + revocation, rescission, cancellation, termination, or any other legal + or equitable action to disrupt the quiet enjoyment of the Work by the + public as contemplated by Affirmer's express Statement of Purpose. + + 3. Public License Fallback. Should any part of the Waiver for any + reason be judged legally invalid or ineffective under applicable law, + then the Waiver shall be preserved to the maximum extent permitted + taking into account Affirmer's express Statement of Purpose. In + addition, to the extent the Waiver is so judged Affirmer hereby grants + to each affected person a royalty-free, non transferable, non + sublicensable, non exclusive, irrevocable and unconditional license to + exercise Affirmer's Copyright and Related Rights in the Work (i) in + all territories worldwide, (ii) for the maximum duration provided by + applicable law or treaty (including future time extensions), (iii) in + any current or future medium and for any number of copies, and (iv) + for any purpose whatsoever, including without limitation commercial, + advertising or promotional purposes (the "License"). The License shall + be deemed effective as of the date CC0 was applied by Affirmer to the + Work. Should any part of the License for any reason be judged legally + invalid or ineffective under applicable law, such partial invalidity + or ineffectiveness shall not invalidate the remainder of the License, + and in such case Affirmer hereby affirms that he or she will not (i) + exercise any of his or her remaining Copyright and Related Rights in + the Work or (ii) assert any associated claims and causes of action + with respect to the Work, in either case contrary to Affirmer's + express Statement of Purpose. + + 4. Limitations and Disclaimers. + + No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. + + Contributions thanks to: + niXman + Ely Arzhannikov + Alexey Pavlov + Ray Donnelly + Johannes Schindelin + +*/ + +#include "winsup.h" +#include "miscfuncs.h" +#include +#include +#include +#include +#include +#include "cygerrno.h" +#include "security.h" +#include "path.h" +#include "fhandler.h" +#include "dtable.h" +#include "cygheap.h" +#include "shared_info.h" +#include "cygtls.h" +#include "tls_pbuf.h" +#include "environ.h" +#include +#include +#include +#include + +#include "msys2_path_conv.h" + +typedef enum PATH_TYPE_E { + NONE = 0, + SIMPLE_WINDOWS_PATH, + ESCAPE_WINDOWS_PATH, + WINDOWS_PATH_LIST, + UNC, + ESCAPED_PATH, + ROOTED_PATH, + POSIX_PATH_LIST, + RELATIVE_PATH, + URL +} path_type; + +int is_special_posix_path(const char* from, const char* to, char** dst, const char* dstend); +void posix_to_win32_path(const char* from, const char* to, char** dst, const char* dstend); + + +path_type find_path_start_and_type(const char** src, int recurse, const char* end); +void copy_to_dst(const char* from, const char* to, char** dst, const char* dstend); +void convert_path(const char** from, const char* to, path_type type, char** dst, const char* dstend); + +//Transformations +//SIMPLE_WINDOWS_PATH converter. Copy as is. Hold C:\Something\like\this +void swp_convert(const char** from, const char* to, char** dst, const char* dstend); +//ESCAPE_WINDOWS_PATH converter. Turn backslashes to slashes and skip first /. Hold /C:\Somethind\like\this +void ewp_convert(const char** from, const char* to, char** dst, const char* dstend); +//WINDOWS_PATH_LIST converter. Copy as is. Hold /something/like/this; +void wpl_convert(const char** from, const char* to, char** dst, const char* dstend); +//UNC convert converter. Copy as is. Hold //somethig/like/this +void unc_convert(const char** from, const char* to, char** dst, const char* dstend); +//ESCAPED_PATH converter. Turn backslashes to slashes and skip first /. Hold //something\like\this +void ep_convert(const char** from, const char* to, char** dst, const char* dstend); +//ROOTED_PATH converter. Prepend root dir to front. Hold /something/like/this +void rp_convert(const char** from, const char* to, char** dst, const char* dstend); +//URL converter. Copy as is. +void url_convert(const char** from, const char* to, char** dst, const char* dstend); +//POSIX_PATH_LIST. Hold x::x/y:z +void ppl_convert(const char** from, const char* to, char** dst, const char* dstend); + + +void find_end_of_posix_list(const char** to, int* in_string) { + for (; **to != '\0' && (!in_string || **to != *in_string); ++*to) { + } + + if (**to == *in_string) { + *in_string = 0; + } +} + +void find_end_of_rooted_path(const char** from, const char** to, int* in_string) { + for (const char* it = *from; *it != '\0' && it != *to; ++it) + if (*it == '.' && *(it + 1) == '.' && *(it - 1) == '/') { + *to = it - 1; + return; + } + + for (; **to != '\0'; ++*to) { + if (*in_string == 0 && **to == ' ') { + return; + } + + if (**to == *in_string) { + *in_string = 0; + return; + } + + if (**to == '/') { + if (*(*to - 1) == ' ') { + *to -= 1; + return; + } + } + } +} + +void sub_convert(const char** from, const char** to, char** dst, const char* dstend, int* in_string) { + const char* copy_from = *from; + path_type type = find_path_start_and_type(from, false, *to); + debug_printf("found type %d for path %s", type, copy_from); + + if (type == POSIX_PATH_LIST) { + find_end_of_posix_list(to, in_string); + } + + if (type == ROOTED_PATH) { + find_end_of_rooted_path(from, to, in_string); + } + + copy_to_dst(copy_from, *from, dst, dstend); + + if (type != NONE) { + convert_path(from, *to, type, dst, dstend); + } + + if (*dst != dstend) { + **dst = **to; + *dst += 1; + } +} + +const char* convert(char *dst, size_t dstlen, const char *src) { + if (dst == NULL || dstlen == 0 || src == NULL) { + return dst; + } + + int need_convert = false; + for (const char* it = src; *it != '\0'; ++it) { + if (*it == '\\' || *it == '/') { + need_convert = true; + break; + } + if (isspace(*it)) { + need_convert = false; + break; + } + } + + char* dstit = dst; + char* dstend = dst + dstlen; + if (!need_convert) { + copy_to_dst(src, NULL, &dstit, dstend); + *dstit = '\0'; + return dst; + } + *dstend = '\0'; + + const char* srcit = src; + const char* srcbeg = src; + + int in_string = false; + + for (; *srcit != '\0'; ++srcit) { + if (*srcit == '\'' || *srcit == '"') { + if (in_string == *srcit) { + if (*(srcit + 1) != in_string) { + in_string = 0; + } + } else { + in_string = *srcit; + } + continue; + } + } + + sub_convert(&srcbeg, &srcit, &dstit, dstend, &in_string); + if (!*srcit) { + *dstit = '\0'; + return dst; + } + srcbeg = srcit + 1; + for (; *srcit != '\0'; ++srcit) { + continue; + } + copy_to_dst(srcbeg, srcit, &dstit, dstend); + *dstit = '\0'; + + /*if (dstit - dst < 2) { + dstit = dst; + copy_to_dst(src, NULL, &dstit, dstend); + *dstit = '\0'; + }*/ + + return dst; +} + +void copy_to_dst(const char* from, const char* to, char** dst, const char* dstend) { + for (; (*from != '\0') && (from != to) && (*dst != dstend); ++from, ++(*dst)) { + **dst = *from; + } +} + +const char** move(const char** p, int count) { + *p += count; + return p; +} + +path_type find_path_start_and_type(const char** src, int recurse, const char* end) { + const char* it = *src; + + if (*it == '\0' || it == end) return NONE; + + /* + * Skip path mangling when environment indicates it. + */ + const char *no_pathconv = getenv ("MSYS_NO_PATHCONV"); + + if (no_pathconv) { + *src = end; + return NONE; + } + + /* Let's not convert ~/.file to ~C:\msys64\.file */ + if (*it == '~') { +skip_p2w: + *src = end; + return NONE; + } + + /* + * Prevent Git's :file.txt and :/message syntax from beeing modified. + */ + if (*it == ':') + goto skip_p2w; + + while (it != end && *it) { + switch (*it) { + case '`': + case '\'': + case '*': + case '?': + case '[': + case ']': + goto skip_p2w; + case '/': + if (it + 1 < end && it[1] == '~') + goto skip_p2w; + break; + case ':': + // Avoid mangling IPv6 addresses + if (it + 1 < end && it[1] == ':') + goto skip_p2w; + + // Leave Git's :./name syntax alone + if (it + 1 < end && it[1] == '.') { + if (it + 2 < end && it[2] == '/') + goto skip_p2w; + if (it + 3 < end && it[2] == '.' && it[3] == '/') + goto skip_p2w; + } + break; + case '@': + // Paths do not contain '@@' + if (it + 1 < end && it[1] == '@') + goto skip_p2w; + } + ++it; + } + it = *src; + + while (!isalnum(*it) && *it != '/' && *it != '\\' && *it != ':' && *it != '-' && *it != '.') { + recurse = true; + it = ++*src; + if (it == end || *it == '\0') return NONE; + } + + path_type result = NONE; + + if (it + 1 == end) { + switch (*it) { + case '/': return ROOTED_PATH ; + default: return SIMPLE_WINDOWS_PATH; + } + } + + if (isalpha(*it) && *(it + 1) == ':') { + if (*(it + 2) == '\\') { + return SIMPLE_WINDOWS_PATH; + } + + if (*(it + 2) == '/' && memchr(it + 2, ':', end - (it + 2)) == NULL) { + return SIMPLE_WINDOWS_PATH; + } + + if (*(it + 2) == '/' && memchr(it + 2, ';', end - (it + 2))) { + return WINDOWS_PATH_LIST; + } + } + + if (*it == '.' && (*(it + 1) == '.' || *(it + 1) == '/') && memchr(it + 2, ':', end - (it + 2)) == NULL) { + return RELATIVE_PATH; + } + + if (*it == '/') { + it += 1; + + if (isalpha(*it) && *(it + 1) == ':') { + return ESCAPE_WINDOWS_PATH; + } + + if (*it == '.' && *(it + 1) == '.') { + return SIMPLE_WINDOWS_PATH; + } + + if (*it == '/') { + it += 1; + switch(*it) { + case ':': return URL; + case '/': return ESCAPED_PATH; + } + if (memchr(it, '/', end - it)) + return UNC; + else + return ESCAPED_PATH; + } + + for (; *it != '\0' && it != end; ++it) { + switch(*it) { + case ':': {char ch = *(it + 1); if (ch == '/' || ch == ':' || ch == '.') return POSIX_PATH_LIST;} return WINDOWS_PATH_LIST; + case ';': return WINDOWS_PATH_LIST; + } + } + + if (result != NONE) { + return result; + } + + return ROOTED_PATH; + } + + int starts_with_minus = 0; + int starts_with_minus_alpha = 0; + int only_dots = *it == '.'; + int has_slashes = 0; + if (*it == '-') { + starts_with_minus = 1; + it += 1; + if (isalpha(*it)) { + it += 1; + starts_with_minus_alpha = 1; + if (memchr(it, ';', end - it)) { + return WINDOWS_PATH_LIST; + } + } + } + + for (const char* it2 = it; *it2 != '\0' && it2 != end; ++it2) { + char ch = *it2; + if (starts_with_minus_alpha) { + if (isalpha(ch) && (*(it2+1) == ':') && (*(it2+2) == '/')) { + return SIMPLE_WINDOWS_PATH; + } + if (ch == '/'&& memchr(it2, ',', end - it2) == NULL) { + *src = it2; + return find_path_start_and_type(src, true, end); + } + starts_with_minus_alpha = 0; + } + if (ch == '\'' || ch == '"') + starts_with_minus = false; + if ((ch == '=') || (ch == ':' && starts_with_minus) || ((ch == '\'' || ch == '"') && result == NONE)) { + *src = it2 + 1; + return find_path_start_and_type(src, true, end); + } + + if (ch == ',' && starts_with_minus) { + *src = it2 + 1; + return find_path_start_and_type(src, true, end); + } + + if (ch == ':' && it2 + 1 != end) { + it2 += 1; + ch = *it2; + if (ch == '/' || ch == ':' || ch == '.') { + if (ch == '/' && *(it2 + 1) == '/') { + return URL; + } else { + if (!only_dots && !has_slashes) + goto skip_p2w; + return POSIX_PATH_LIST; + } + } else if (memchr(it2, '=', end - it2) == NULL) { + return SIMPLE_WINDOWS_PATH; + } + } else if (ch != '.') { + only_dots = 0; + if (ch == '/' || ch == '\\') + has_slashes = 1; + } + } + + if (result != NONE) { + *src = it; + return result; + } + + return SIMPLE_WINDOWS_PATH; +} + +void convert_path(const char** from, const char* to, path_type type, char** dst, const char* dstend) { + switch(type) { + case SIMPLE_WINDOWS_PATH: swp_convert(from, to, dst, dstend); break; + case ESCAPE_WINDOWS_PATH: ewp_convert(from, to, dst, dstend); break; + case WINDOWS_PATH_LIST: wpl_convert(from, to, dst, dstend); break; + case RELATIVE_PATH: swp_convert(from, to, dst, dstend); break; + case UNC: unc_convert(from, to, dst, dstend); break; + case ESCAPED_PATH: ep_convert(from, to, dst, dstend); break; + case ROOTED_PATH: rp_convert(from, to, dst, dstend); break; + case URL: url_convert(from, to, dst, dstend); break; + case POSIX_PATH_LIST: ppl_convert(from, to, dst, dstend); break; + case NONE: // prevent warnings; + default: + return; + } +} + +void swp_convert(const char** from, const char* to, char** dst, const char* dstend) { + copy_to_dst(*from, to, dst, dstend); +} + +void ewp_convert(const char** from, const char* to, char** dst, const char* dstend) { + *from += 1; + unc_convert(from, to, dst, dstend); +} + +void wpl_convert(const char** from, const char* to, char** dst, const char* dstend) { + swp_convert(from, to, dst, dstend); +} + +void unc_convert(const char** from, const char* to, char** dst, const char* dstend) { + const char* it = *from; + for (; (*it != '\0' && it != to) && (*dst != dstend); ++it, ++(*dst)) { + if (*it == '\\') { + **dst = '/'; + } else { + **dst = *it; + } + } +} + +void ep_convert(const char** from, const char* to, char** dst, const char* dstend) { + ewp_convert(from, to, dst, dstend); +} + +void rp_convert(const char** from, const char* to, char** dst, const char* dstend) { + const char* it = *from; + const char* real_to = to; + + if (*real_to == '\0') { + real_to -= 1; + if (*real_to != '\'' && *real_to != '"') { + real_to += 1; + } + } + + if (!is_special_posix_path(*from, real_to, dst, dstend)) { + posix_to_win32_path(it, real_to, dst, dstend); + } + + if (*dst != dstend && real_to != to) { + **dst = *real_to; + *dst += 1; + } +} + +void url_convert(const char** from, const char* to, char** dst, const char* dstend) { + unc_convert(from, to, dst, dstend); +} + +void subp_convert(const char** from, const char* end, int is_url, char** dst, const char* dstend) { + const char* begin = *from; + path_type type = is_url ? URL : find_path_start_and_type(from, 0, end); + copy_to_dst(begin, *from, dst, dstend); + + if (type == NONE) { + return; + } + + char* start = *dst; + convert_path(from, end, type, dst, dstend); + + if (!is_url) { + for (; start != *dst; ++start) { + if (*start == '/') { + *start = '\\'; + } + } + } +} + +void ppl_convert(const char** from, const char* to, char** dst, const char* dstend) { + const char *orig_dst = *dst; + const char* it = *from; + const char* beg = it; + int prev_was_simc = 0; + int is_url = 0; + for (; (*it != '\0' && it != to) && (*dst != dstend); ++it) { + if (*it == ':') { + if (prev_was_simc) { + continue; + } + if (*(it + 1) == '/' && *(it + 2) == '/' && isalpha(*beg)) { + is_url = 1; + /* double-check: protocol must be alnum (or +) */ + for (const char *p = beg; p != it; ++p) + if (!isalnum(*p) && *p != '+') { + is_url = 0; + break; + } + if (is_url) + continue; + } + prev_was_simc = 1; + subp_convert(&beg, it, is_url, dst, dstend); + is_url = 0; + + if (*dst == dstend) { + system_printf("Path cut off during conversion: %s\n", orig_dst); + break; + } + + **dst = ';'; + *dst += 1; + } + + if (*it != ':' && prev_was_simc) { + prev_was_simc = 0; + beg = it; + } + } + + if (!prev_was_simc) { + subp_convert(&beg, it, is_url, dst, dstend); + } +} + +int is_special_posix_path(const char* from, const char* to, char** dst, const char* dstend) { + const char dev_null[] = "/dev/null"; + + if ((to - from) == (sizeof(dev_null) - 1) && strncmp(from, "/dev/null", to - from) == 0) { + copy_to_dst("nul", NULL, dst, dstend); + return true; + } + return false; +} + +void posix_to_win32_path(const char* from, const char* to, char** dst, const char* dstend) { + if ( from != to ) { + tmp_pathbuf tp; + char *one_path = tp.c_get(); + strncpy(one_path, from, to-from); + one_path[to-from] = '\0'; + + path_conv conv (one_path, PC_NOFULL); + if (conv.error) + { + set_errno(conv.error); + copy_to_dst(one_path, NULL, dst, dstend); + } else { + char* win32_path = tp.c_get(); + stpcpy (win32_path, conv.get_win32 ()); + for (; (*win32_path != '\0') && (*dst != dstend); ++win32_path, ++(*dst)) { + **dst = (*win32_path == '\\') ? '/' : *win32_path; + } + } + } +} + diff --git a/winsup/cygwin/msys2_path_conv.h b/winsup/cygwin/msys2_path_conv.h new file mode 100644 index 0000000000..67d85ecb64 --- /dev/null +++ b/winsup/cygwin/msys2_path_conv.h @@ -0,0 +1,147 @@ +/* + The MSYS2 Path conversion source code is licensed under: + + CC0 1.0 Universal + + Official translations of this legal tool are available + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + + Statement of Purpose + + The laws of most jurisdictions throughout the world automatically + confer exclusive Copyright and Related Rights (defined below) upon the + creator and subsequent owner(s) (each and all, an "owner") of an + original work of authorship and/or a database (each, a "Work"). + + Certain owners wish to permanently relinquish those rights to a Work + for the purpose of contributing to a commons of creative, cultural and + scientific works ("Commons") that the public can reliably and without + fear of later claims of infringement build upon, modify, incorporate + in other works, reuse and redistribute as freely as possible in any + form whatsoever and for any purposes, including without limitation + commercial purposes. These owners may contribute to the Commons to + promote the ideal of a free culture and the further production of + creative, cultural and scientific works, or to gain reputation or + greater distribution for their Work in part through the use and + efforts of others. + + For these and/or other purposes and motivations, and without any + expectation of additional consideration or compensation, the person + associating CC0 with a Work (the "Affirmer"), to the extent that he or + she is an owner of Copyright and Related Rights in the Work, + voluntarily elects to apply CC0 to the Work and publicly distribute + the Work under its terms, with knowledge of his or her Copyright and + Related Rights in the Work and the meaning and intended legal effect + of CC0 on those rights. + + 1. Copyright and Related Rights. A Work made available under CC0 may + be protected by copyright and related or neighboring rights + ("Copyright and Related Rights"). Copyright and Related Rights + include, but are not limited to, the following: + + the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + moral rights retained by the original author(s) and/or performer(s); + publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + rights protecting the extraction, dissemination, use and reuse of data + in a Work; + database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and + other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations + thereof. + + 2. Waiver. To the greatest extent permitted by, but not in + contravention of, applicable law, Affirmer hereby overtly, fully, + permanently, irrevocably and unconditionally waives, abandons, and + surrenders all of Affirmer's Copyright and Related Rights and + associated claims and causes of action, whether now known or unknown + (including existing as well as future claims and causes of action), in + the Work (i) in all territories worldwide, (ii) for the maximum + duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number + of copies, and (iv) for any purpose whatsoever, including without + limitation commercial, advertising or promotional purposes (the + "Waiver"). Affirmer makes the Waiver for the benefit of each member of + the public at large and to the detriment of Affirmer's heirs and + successors, fully intending that such Waiver shall not be subject to + revocation, rescission, cancellation, termination, or any other legal + or equitable action to disrupt the quiet enjoyment of the Work by the + public as contemplated by Affirmer's express Statement of Purpose. + + 3. Public License Fallback. Should any part of the Waiver for any + reason be judged legally invalid or ineffective under applicable law, + then the Waiver shall be preserved to the maximum extent permitted + taking into account Affirmer's express Statement of Purpose. In + addition, to the extent the Waiver is so judged Affirmer hereby grants + to each affected person a royalty-free, non transferable, non + sublicensable, non exclusive, irrevocable and unconditional license to + exercise Affirmer's Copyright and Related Rights in the Work (i) in + all territories worldwide, (ii) for the maximum duration provided by + applicable law or treaty (including future time extensions), (iii) in + any current or future medium and for any number of copies, and (iv) + for any purpose whatsoever, including without limitation commercial, + advertising or promotional purposes (the "License"). The License shall + be deemed effective as of the date CC0 was applied by Affirmer to the + Work. Should any part of the License for any reason be judged legally + invalid or ineffective under applicable law, such partial invalidity + or ineffectiveness shall not invalidate the remainder of the License, + and in such case Affirmer hereby affirms that he or she will not (i) + exercise any of his or her remaining Copyright and Related Rights in + the Work or (ii) assert any associated claims and causes of action + with respect to the Work, in either case contrary to Affirmer's + express Statement of Purpose. + + 4. Limitations and Disclaimers. + + No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. + + Contributions thanks to: + niXman + Ely Arzhannikov + Alexey Pavlov + Ray Donnelly + Johannes Schindelin + +*/ + +#ifndef PATH_CONV_H_DB4IQBH3 +#define PATH_CONV_H_DB4IQBH3 + +#include + +const char* convert(char *dst, size_t dstlen, const char *src); + +#endif /* end of include guard: PATH_CONV_H_DB4IQBH3 */ + diff --git a/winsup/cygwin/path.cc b/winsup/cygwin/path.cc index 405d4ba29f..b31dfe442e 100644 --- a/winsup/cygwin/path.cc +++ b/winsup/cygwin/path.cc @@ -66,6 +66,7 @@ #include "shared_info.h" #include "tls_pbuf.h" #include "environ.h" +#include "msys2_path_conv.h" #undef basename suffix_info stat_suffixes[] = @@ -3898,6 +3899,74 @@ fchdir (int fd) return res; } +// +// Important: If returned pointer == arg, then this function +// did not malloc that pointer; otherwise free it. +// +extern "C" char * +arg_heuristic_with_exclusions (char const * const arg, char const * exclusions, size_t exclusions_count) +{ + char *arg_result; + + // Must return something .. + size_t arglen = (arg ? strlen (arg): 0); + + if (arglen == 0 || !arg) + { + arg_result = (char *)malloc (sizeof (char)); + arg_result[0] = '\0'; + return arg_result; + } + + debug_printf("Input value: (%s)", arg); + for (size_t excl = 0; excl < exclusions_count; ++excl) + { + /* Since we've got regex linked we should maybe switch to that, but + running regexes for every argument could be too slow. */ + if ( strcmp (exclusions, "*") == 0 || (strlen (exclusions) && strstr (arg, exclusions) == arg) ) + return (char*)arg; + exclusions += strlen (exclusions) + 1; + } + + // Leave enough room for at least 16 path elements; we might be converting + // a path list. + size_t stack_len = arglen + 16 * MAX_PATH; + char * stack_path = (char *)malloc (stack_len); + if (!stack_path) + { + debug_printf ("out of stack space?"); + return (char *)arg; + } + memset (stack_path, 0, MAX_PATH); + convert (stack_path, stack_len - 1, arg); + debug_printf ("convert()'ed: %s (length %d)\n.....->: %s", arg, arglen, stack_path); + // Don't allocate memory if no conversion happened. + if (!strcmp (arg, stack_path)) + { + if (arg != stack_path) + { + free (stack_path); + } + return ((char *)arg); + } + arg_result = (char *)realloc (stack_path, strlen (stack_path)+1); + // Windows doesn't like empty entries in PATH env. variables (;;) + char* semisemi = strstr(arg_result, ";;"); + while (semisemi) + { + memmove(semisemi, semisemi+1, strlen(semisemi)); + semisemi = strstr(semisemi, ";;"); + } + return arg_result; +} + +extern "C" char * +arg_heuristic (char const * const arg) +{ + return arg_heuristic_with_exclusions (arg, NULL, 0); +} + + /******************** Exported Path Routines *********************/ /* Cover functions to the path conversion routines. diff --git a/winsup/cygwin/spawn.cc b/winsup/cygwin/spawn.cc index 81b99e7633..37137bd7a2 100644 --- a/winsup/cygwin/spawn.cc +++ b/winsup/cygwin/spawn.cc @@ -286,6 +286,27 @@ child_info_spawn::worker (const char *prog_arg, const char *const *argv, bool rc; int res = -1; + /* Environment variable MSYS2_ARG_CONV_EXCL contains a list + of ';' separated argument prefixes to pass un-modified.. + It isn't applied to env. variables; only spawn arguments. + A value of * means don't convert any arguments. */ + char* msys2_arg_conv_excl_env = getenv("MSYS2_ARG_CONV_EXCL"); + char* msys2_arg_conv_excl = NULL; + size_t msys2_arg_conv_excl_count = 0; + if (msys2_arg_conv_excl_env) + { + msys2_arg_conv_excl = (char*)alloca (strlen(msys2_arg_conv_excl_env)+1); + strcpy (msys2_arg_conv_excl, msys2_arg_conv_excl_env); + msys2_arg_conv_excl_count = 1; + msys2_arg_conv_excl_env = strchr ( msys2_arg_conv_excl, ';' ); + while (msys2_arg_conv_excl_env) + { + *msys2_arg_conv_excl_env = '\0'; + ++msys2_arg_conv_excl_count; + msys2_arg_conv_excl_env = strchr ( msys2_arg_conv_excl_env + 1, ';' ); + } + } + /* Check if we have been called from exec{lv}p or spawn{lv}p and mask mode to keep only the spawn mode. */ bool p_type_exec = !!(mode & _P_PATH_TYPE_EXEC); @@ -377,6 +398,20 @@ child_info_spawn::worker (const char *prog_arg, const char *const *argv, moreinfo->argc = newargv.argc; moreinfo->argv = newargv; } + else + { + for (int i = 0; i < newargv.argc; i++) + { + //convert argv to win32 + int newargvlen = strlen (newargv[i]); + char *tmpbuf = (char *)malloc (newargvlen + 1); + memcpy (tmpbuf, newargv[i], newargvlen + 1); + tmpbuf = arg_heuristic_with_exclusions(tmpbuf, msys2_arg_conv_excl, msys2_arg_conv_excl_count); + debug_printf("newargv[%d] = %s", i, newargv[i]); + newargv.replace (i, tmpbuf); + free (tmpbuf); + } + } if ((wincmdln || !real_path.iscygexec ()) && !cmd.fromargv (newargv, real_path.get_win32 (), real_path.iscygexec ())) @@ -511,7 +546,8 @@ child_info_spawn::worker (const char *prog_arg, const char *const *argv, moreinfo->envp = build_env (envp, envblock, moreinfo->envc, real_path.iscygexec (), switch_user ? ::cygheap->user.primary_token () - : NULL); + : NULL, + real_path.iscygexec ()); if (!moreinfo->envp || !envblock) { set_errno (E2BIG); From 874d25617ea9e3f522f8d701a0ae43ee79bc8a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B8=CC=86=20=D0=9F?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=BE=D0=B2?= Date: Sun, 14 Apr 2019 21:29:01 +0300 Subject: [PATCH 034/102] Add functionality for changing OS name via MSYSTEM environment variables. --- winsup/cygserver/cygserver-config | 4 ++-- winsup/cygwin/environ.cc | 34 ++++++++++++++++++++++++++--- winsup/cygwin/include/sys/utsname.h | 2 +- winsup/cygwin/uname.cc | 17 +++++++++++++-- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/winsup/cygserver/cygserver-config b/winsup/cygserver/cygserver-config index 3130de7bcd..6fc3e06ae0 100755 --- a/winsup/cygserver/cygserver-config +++ b/winsup/cygserver/cygserver-config @@ -86,7 +86,7 @@ done # Check if running on NT _sys="`uname`" -_nt=`expr "${_sys}" : "CYGWIN_NT"` +_nt=`expr "${_sys}" : "MSYS_NT"` # Check for running cygserver processes first. if ps -e | grep -v grep | grep -q ${service_name} @@ -178,7 +178,7 @@ then echo "Do you want to install cygserver as service?" if request "(Say \"no\" if it's already installed as service)" then - if ! cygrunsrv -I ${service_name} -d "CYGWIN cygserver" -p /usr/sbin/cygserver + if ! cygrunsrv -I ${service_name} -d "MSYS cygserver" -p /usr/sbin/cygserver then echo echo "Installation of cygserver as service failed. Please check the" diff --git a/winsup/cygwin/environ.cc b/winsup/cygwin/environ.cc index 639e69393b..b9f7e05452 100644 --- a/winsup/cygwin/environ.cc +++ b/winsup/cygwin/environ.cc @@ -192,7 +192,11 @@ parse_options (const char *inbuf) if (export_settings) { debug_printf ("%s", newbuf + 1); +#ifdef __MSYS__ + setenv ("MSYS", newbuf + 1, 1); +#else setenv ("CYGWIN", newbuf + 1, 1); +#endif } return; } @@ -651,7 +655,7 @@ _addenv (const char *name, const char *value, int overwrite) win_env *spenv; if ((spenv = getwinenv (envhere))) spenv->add_cache (value); - if (strcmp (name, "CYGWIN") == 0) + if (strcmp (name, "MSYS") == 0) parse_options (value); return 0; @@ -754,6 +758,9 @@ static struct renv { } renv_arr[] = { { NL("COMMONPROGRAMFILES=") }, // 0 { NL("COMSPEC=") }, +#ifdef __MSYS__ + { NL("MSYSTEM=") }, // 2 +#endif /* __MSYS__ */ { NL("PATH=") }, // 2 { NL("PROGRAMFILES=") }, { NL("SYSTEMDRIVE=") }, // 4 @@ -765,10 +772,21 @@ static struct renv { #define RENV_SIZE (sizeof (renv_arr) / sizeof (renv_arr[0])) /* Set of first characters of the above list of variables. */ -static const char idx_arr[] = "CPSTW"; +static const char idx_arr[] = +#ifdef __MSYS__ + "CMPSTW"; +#else + "CPSTW"; +#endif /* Index into renv_arr at which the variables with this specific character starts. */ -static const int start_at[] = { 0, 2, 4, 6, 8 }; +static const int start_at[] = { +#ifdef __MSYS__ + 0, 2, 3, 5, 7, 9 +#else + 0, 2, 4, 6, 8 +#endif + }; /* Turn environment variable part of a=b string into uppercase - for some environment variables only. */ @@ -836,7 +854,11 @@ environ_init (char **envp, int envc) dumper_init (); if (envp_passed_in) { +#ifdef __MSYS__ + p = getenv ("MSYS"); +#else p = getenv ("CYGWIN"); +#endif if (p) parse_options (p); } @@ -883,8 +905,13 @@ win32env_to_cygenv (PWCHAR rawenv, bool posify) ucenv (newp, eq); /* uppercase env vars which need it */ if (*newp == 'T' && strncmp (newp, "TERM=", 5) == 0) sawTERM = 1; +#ifdef __MSYS__ + else if (*newp == 'M' && strncmp (newp, "MSYS=", 5) == 0) + parse_options (newp + 5); +#else else if (*newp == 'C' && strncmp (newp, "CYGWIN=", 7) == 0) parse_options (newp + 7); +#endif if (*eq && posify) posify_maybe (envp + i, *++eq ? eq : --eq, tmpbuf); debug_printf ("%p: %s", envp[i], envp[i]); @@ -959,6 +986,7 @@ static NO_COPY spenv spenvs[] = {NL ("HOMEPATH="), false, false, &cygheap_user::env_homepath}, {NL ("LOGONSERVER="), false, false, &cygheap_user::env_logsrv}, {NL ("PATH="), false, true, NULL}, + {NL ("MSYSTEM="), true, true, NULL}, {NL ("SYSTEMDRIVE="), false, true, NULL}, {NL ("SYSTEMROOT="), true, true, &cygheap_user::env_systemroot}, {NL ("USERDOMAIN="), false, false, &cygheap_user::env_domain}, diff --git a/winsup/cygwin/include/sys/utsname.h b/winsup/cygwin/include/sys/utsname.h index d6b3be96f7..730cb731a5 100644 --- a/winsup/cygwin/include/sys/utsname.h +++ b/winsup/cygwin/include/sys/utsname.h @@ -17,7 +17,7 @@ extern "C" { struct utsname { - char sysname[_UTSNAME_LENGTH]; + char sysname[_UTSNAME_LENGTH + 1]; char nodename[_UTSNAME_LENGTH]; char release[_UTSNAME_LENGTH]; char version[_UTSNAME_LENGTH]; diff --git a/winsup/cygwin/uname.cc b/winsup/cygwin/uname.cc index c08e30f97d..ed4c9c59a1 100644 --- a/winsup/cygwin/uname.cc +++ b/winsup/cygwin/uname.cc @@ -37,7 +37,12 @@ uname_x (struct utsname *name) memset (name, 0, sizeof (*name)); /* sysname */ - n = __small_sprintf (name->sysname, "CYGWIN_%s-%u", + char* msystem = getenv("MSYSTEM"); + const char* msystem_sysname = "MSYS"; + if (msystem != NULL && *msystem && strcmp(msystem, "MSYS") != 0) + msystem_sysname = (strstr(msystem, "32") != NULL) ? "MINGW32" : "MINGW64";; + n = __small_sprintf (name->sysname, "%s_%s-%u", + msystem_sysname, wincap.osname (), wincap.build_number ()); if (wincap.host_machine () != wincap.cygwin_machine ()) { @@ -104,7 +109,7 @@ uname_x (struct utsname *name) /* Old entrypoint for applications up to API 334 */ struct old_utsname { - char sysname[20]; + char sysname[21]; char nodename[20]; char release[20]; char version[20]; @@ -118,7 +123,15 @@ uname (struct utsname *in_name) __try { memset (name, 0, sizeof (*name)); +#ifdef __MSYS__ + char* msystem = getenv("MSYSTEM"); + const char* msystem_sysname = "MSYS"; + if (msystem != NULL && *msystem && strcmp(msystem, "MSYS") != 0) + msystem_sysname = (strstr(msystem, "32") != NULL) ? "MINGW32" : "MINGW64"; + __small_sprintf (name->sysname, "%s_%s", msystem_sysname, wincap.osname ()); +#else __small_sprintf (name->sysname, "CYGWIN_%s", wincap.osname ()); +#endif /* Computer name */ cygwin_gethostname (name->nodename, sizeof (name->nodename) - 1); From 67e0663d5a6b3ba59a672c430dcab9152bd19d87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B8=CC=86=20=D0=9F?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=BE=D0=B2?= Date: Sun, 14 Apr 2019 21:45:06 +0300 Subject: [PATCH 035/102] - Move root to /usr. - Change sorting mount points. - By default mount without ACLs. - Can read /etc/fstab with short mount point format. --- winsup/cygwin/local_includes/mount.h | 3 +- winsup/cygwin/mm/cygheap.cc | 12 +- winsup/cygwin/mount.cc | 185 +++++++++++++++++++++++---- winsup/cygwin/uinfo.cc | 2 +- 4 files changed, 174 insertions(+), 28 deletions(-) diff --git a/winsup/cygwin/local_includes/mount.h b/winsup/cygwin/local_includes/mount.h index 163b47551f..15e9a342ba 100644 --- a/winsup/cygwin/local_includes/mount.h +++ b/winsup/cygwin/local_includes/mount.h @@ -173,7 +173,6 @@ class mount_info mount_item mount[MAX_MOUNTS]; static bool got_usr_bin; - static bool got_usr_lib; static int root_idx; /* cygdrive_prefix is used as the root of the path automatically @@ -185,6 +184,8 @@ class mount_info private: int posix_sorted[MAX_MOUNTS]; int native_sorted[MAX_MOUNTS]; + int longest_posix_sorted[MAX_MOUNTS]; + int shortest_native_sorted[MAX_MOUNTS]; public: void init (bool); diff --git a/winsup/cygwin/mm/cygheap.cc b/winsup/cygwin/mm/cygheap.cc index 1c9b8037b2..4a60995c44 100644 --- a/winsup/cygwin/mm/cygheap.cc +++ b/winsup/cygwin/mm/cygheap.cc @@ -220,14 +220,22 @@ init_cygheap::init_installation_root () /* Strip off last path component ("\\cygwin1.dll") */ PWCHAR w = wcsrchr (installation_root_buf, L'\\'); +#ifdef __MSYS__ + /* Back two folders to get root as we have all stuff in usr subfolder */ + for (int i=1; i >=0; --i) + { +#endif if (w) { *w = L'\0'; w = wcsrchr (installation_root_buf, L'\\'); } if (!w) - api_fatal ("Can't initialize Cygwin installation root dir.\n" + api_fatal ("Can't initialize MSYS2 installation root dir.\n" "Invalid DLL path"); +#ifdef __MSYS__ + } +#endif /* Copy result into installation_dir before stripping off "bin" dir and revert to Win32 path. This path is added to the Windows environment @@ -252,6 +260,7 @@ init_cygheap::init_installation_root () RtlInitUnicodeString (&installation_root, installation_root_buf); RtlInitUnicodeString (&installation_dir, installation_dir_buf); +#ifndef __MSYS__ for (int i = 1; i >= 0; --i) { reg_key r (i, KEY_WRITE, _WIDE (CYGWIN_INFO_INSTALLATIONS_NAME), @@ -260,6 +269,7 @@ init_cygheap::init_installation_root () installation_root_buf))) break; } +#endif } /* Initialize bucket_val. The value is the max size of a block diff --git a/winsup/cygwin/mount.cc b/winsup/cygwin/mount.cc index 1cfee5c415..affb7e9266 100644 --- a/winsup/cygwin/mount.cc +++ b/winsup/cygwin/mount.cc @@ -42,7 +42,6 @@ details. */ (path_prefix_p (proc, (path), proc_len, false)) bool NO_COPY mount_info::got_usr_bin; -bool NO_COPY mount_info::got_usr_lib; int NO_COPY mount_info::root_idx = -1; /* is_native_path: Return non-zero if PATH starts with \??\[a-zA-Z] or @@ -395,7 +394,6 @@ fs_info::update (PUNICODE_STRING upath, HANDLE in_vol) #define MINIMAL_WIN_NTFS_FLAGS (FILE_CASE_SENSITIVE_SEARCH \ | FILE_CASE_PRESERVED_NAMES \ | FILE_UNICODE_ON_DISK \ - | FILE_PERSISTENT_ACLS \ | FILE_FILE_COMPRESSION \ | FILE_VOLUME_QUOTAS \ | FILE_SUPPORTS_SPARSE_FILES \ @@ -552,13 +550,13 @@ mount_info::create_root_entry (const PWCHAR root) sys_wcstombs (native_root, PATH_MAX, root); assert (*native_root != '\0'); if (add_item (native_root, "/", - MOUNT_SYSTEM | MOUNT_IMMUTABLE | MOUNT_AUTOMATIC) + MOUNT_SYSTEM | MOUNT_IMMUTABLE | MOUNT_AUTOMATIC | MOUNT_NOACL) < 0) api_fatal ("add_item (\"%s\", \"/\", ...) failed, errno %d", native_root, errno); /* Create a default cygdrive entry. Note that this is a user entry. This allows to override it with mount, unless the sysadmin created a cygdrive entry in /etc/fstab. */ - cygdrive_flags = MOUNT_NOPOSIX | MOUNT_CYGDRIVE; + cygdrive_flags = MOUNT_NOPOSIX | MOUNT_CYGDRIVE | MOUNT_NOACL; strcpy (cygdrive, CYGWIN_INFO_CYGDRIVE_DEFAULT_PREFIX "/"); cygdrive_len = strlen (cygdrive); } @@ -578,22 +576,14 @@ mount_info::init (bool user_init) pathend = wcpcpy (pathend, L"\\etc\\fstab"); from_fstab (user_init, path, pathend); - if (!user_init && (!got_usr_bin || !got_usr_lib)) + if (!user_init && !got_usr_bin) { char native[PATH_MAX]; if (root_idx < 0) - api_fatal ("root_idx %d, user_shared magic %y, nmounts %d", root_idx, user_shared->version, nmounts); + api_fatal ("root_idx %d, user_shared magic %y, nmounts %d", root_idx, user_shared->version, nmounts); char *p = stpcpy (native, mount[root_idx].native_path); - if (!got_usr_bin) - { - stpcpy (p, "\\bin"); - add_item (native, "/usr/bin", MOUNT_SYSTEM | MOUNT_AUTOMATIC); - } - if (!got_usr_lib) - { - stpcpy (p, "\\lib"); - add_item (native, "/usr/lib", MOUNT_SYSTEM | MOUNT_AUTOMATIC); - } + stpcpy (p, "\\usr\\bin"); + add_item (native, "/bin", MOUNT_SYSTEM | MOUNT_AUTOMATIC | MOUNT_NOACL); } } @@ -674,6 +664,7 @@ mount_info::conv_to_win32_path (const char *src_path, char *dst, device& dev, /* See if this is a cygwin "device" */ if (win32_device_name (src_path, dst, dev)) { + debug_printf ("win32_device_name (%s)", src_path); *flags = 0; rc = 0; goto out_no_chroot_check; @@ -711,6 +702,7 @@ mount_info::conv_to_win32_path (const char *src_path, char *dst, device& dev, } if (isproc (src_path)) { + debug_printf ("isproc (%s)", src_path); dev = *proc_dev; dev = fhandler_proc::get_proc_fhandler (src_path); if (dev == FH_NADA) @@ -732,6 +724,7 @@ mount_info::conv_to_win32_path (const char *src_path, char *dst, device& dev, off the prefix and transform it into an MS-DOS path. */ else if (iscygdrive (src_path)) { + debug_printf ("iscygdrive (%s) mount_table->cygdrive %s", src_path, mount_table->cygdrive); int n = mount_table->cygdrive_len - 1; int unit; @@ -743,11 +736,15 @@ mount_info::conv_to_win32_path (const char *src_path, char *dst, device& dev, } else if (cygdrive_win32_path (src_path, dst, unit)) { + debug_printf ("cygdrive_win32_path (%s)", src_path); *flags = cygdrive_flags; goto out; } else if (mount_table->cygdrive_len > 1) - return ENOENT; + { + debug_printf ("mount_table->cygdrive_len > 1 (%s)", src_path); + return ENOENT; + } } int chroot_pathlen; @@ -758,7 +755,9 @@ mount_info::conv_to_win32_path (const char *src_path, char *dst, device& dev, const char *path; int len; - mi = mount + posix_sorted[i]; + mi = mount + shortest_native_sorted[i]; + debug_printf (" mount[%d] .. checking %s -> %s ", i, mi->posix_path, mi->native_path); + if (!cygheap->root.exists () || (mi->posix_pathlen == 1 && mi->posix_path[0] == '/')) { @@ -998,7 +997,8 @@ mount_info::conv_to_posix_path (const char *src_path, char *posix_path, int pathbuflen = tail - pathbuf; for (int i = 0; i < nmounts; ++i) { - mount_item &mi = mount[native_sorted[i]]; + mount_item &mi = mount[longest_posix_sorted[i]]; + debug_printf (" mount[%d] .. checking %s -> %s ", i, mi.posix_path, mi.native_path); if (!path_prefix_p (mi.native_path, pathbuf, mi.native_pathlen, mi.flags & MOUNT_NOPOSIX)) continue; @@ -1211,8 +1211,17 @@ mount_info::from_fstab_line (char *line, bool user) if (!*c) return true; cend = find_ws (c); - *cend = '\0'; posix_path = conv_fstab_spaces (c); + if (!*cend) + { + unsigned mount_flags = MOUNT_SYSTEM | MOUNT_NOPOSIX | MOUNT_NOACL; + + int res = mount_table->add_item (native_path, posix_path, mount_flags); + if (res && get_errno () == EMFILE) + return false; + return true; + } + *cend = '\0'; /* Third field: FS type. */ c = skip_ws (cend + 1); if (!*c) @@ -1441,16 +1450,145 @@ sort_by_native_name (const void *a, const void *b) return res; } +/* sort_by_longest_posix_name: qsort callback to sort the mount entries. + Sort user mounts ahead of system mounts to the same POSIX path. */ +/* FIXME: should the user should be able to choose whether to + prefer user or system mounts??? */ +static int +sort_by_longest_posix_name (const void *a, const void *b) +{ + mount_item *ap = mounts_for_sort + (*((int*) a)); + mount_item *bp = mounts_for_sort + (*((int*) b)); + + /* Base weighting on the conversion that would give the longest + posix path. */ + ssize_t alen = (ssize_t) strlen (ap->posix_path) - (ssize_t) strlen (ap->native_path); + ssize_t blen = (ssize_t) strlen (bp->posix_path) - (ssize_t) strlen (bp->native_path); + + int res = blen - alen; + + if (res) + return res; /* Path lengths differed */ + + /* The two paths were the same length, so just determine normal + lexical sorted order. */ + res = strcmp (ap->posix_path, bp->posix_path); + + if (res == 0) + { + /* need to select between user and system mount to same POSIX path */ + if (!(bp->flags & MOUNT_SYSTEM)) /* user mount */ + return 1; + else + return -1; + } + + return res; +} + +/* sort_by_shortest_native_name: qsort callback to sort the mount entries. + Sort user mounts ahead of system mounts to the same POSIX path. */ +/* FIXME: should the user should be able to choose whether to + prefer user or system mounts??? */ +static int +sort_by_shortest_native_name (const void *a, const void *b) +{ + mount_item *ap = mounts_for_sort + (*((int*) a)); + mount_item *bp = mounts_for_sort + (*((int*) b)); + + /* Base weighting on the conversion that would give the shortest + native path. */ + ssize_t alen = (ssize_t) strlen (ap->native_path); + ssize_t blen = (ssize_t) strlen (bp->native_path); + + int res = alen - blen; + + if (res) + return res; /* Path lengths differed */ + + /* The two paths were the same length, so just determine normal + lexical sorted order. */ + res = strcmp (ap->native_path, bp->native_path); + + if (res == 0) + { + /* need to select between user and system mount to same POSIX path */ + if (!(bp->flags & MOUNT_SYSTEM)) /* user mount */ + return 1; + else + return -1; + } + + return res; +} + +static int +sort_posix_subdirs_before_parents (const void *a, const void *b) +{ + mount_item *ap = mounts_for_sort + (*((int*) a)); + mount_item *bp = mounts_for_sort + (*((int*) b)); + + if (ap->posix_pathlen > bp->posix_pathlen) + { + if (!memcmp (bp->posix_path, ap->posix_path, bp->posix_pathlen)) + { + // bp is a subdir of ap (bp must be moved in-front) + return -1; + } + } + else if (ap->posix_pathlen < bp->posix_pathlen) + { + if (!memcmp (ap->posix_path, bp->posix_path, ap->posix_pathlen)) + { + // ap is a subdir of bp (good as we are) + return 1; + } + } + return 0; +} + +#define DISABLE_NEW_STUFF 0 +#define ONLY_USE_NEW_STUFF 1 + void mount_info::sort () { for (int i = 0; i < nmounts; i++) - native_sorted[i] = posix_sorted[i] = i; + native_sorted[i] = posix_sorted[i] = shortest_native_sorted[i] = longest_posix_sorted[i] = i; /* Sort them into reverse length order, otherwise we won't be able to look for /foo in /. */ mounts_for_sort = mount; /* ouch. */ qsort (posix_sorted, nmounts, sizeof (posix_sorted[0]), sort_by_posix_name); qsort (native_sorted, nmounts, sizeof (native_sorted[0]), sort_by_native_name); + qsort (longest_posix_sorted, nmounts, sizeof (longest_posix_sorted[0]), sort_by_longest_posix_name); + qsort (shortest_native_sorted, nmounts, sizeof (shortest_native_sorted[0]), sort_by_shortest_native_name); + qsort (shortest_native_sorted, nmounts, sizeof (shortest_native_sorted[0]), sort_posix_subdirs_before_parents); + /* Disabling my new crap. */ + #if DISABLE_NEW_STUFF + for (int i = 0; i < nmounts; i++) + { + longest_posix_sorted[i] = native_sorted[i]; + shortest_native_sorted[i] = posix_sorted[i]; + } + #else + #if ONLY_USE_NEW_STUFF + for (int i = 0; i < nmounts; i++) + { + native_sorted[i] = longest_posix_sorted[i]; + posix_sorted[i] = shortest_native_sorted[i]; + } + #endif + #endif + for (int i = 0; i < nmounts; i++) + { + mount_item *mi = mount + shortest_native_sorted[i]; + debug_printf ("shortest_native_sorted (subdirs before parents)[%d] %12s %12s", i, mi->native_path, mi->posix_path); + } + for (int i = 0; i < nmounts; i++) + { + mount_item *mi = mount + longest_posix_sorted[i]; + debug_printf ("longest_posix_sorted[%d] %12s %12s", i, mi->native_path, mi->posix_path); + } } /* Add an entry to the mount table. @@ -1541,12 +1679,9 @@ mount_info::add_item (const char *native, const char *posix, if (i == nmounts) nmounts++; - if (strcmp (posixtmp, "/usr/bin") == 0) + if (strcmp (posixtmp, "/bin") == 0) got_usr_bin = true; - if (strcmp (posixtmp, "/usr/lib") == 0) - got_usr_lib = true; - if (posixtmp[0] == '/' && posixtmp[1] == '\0' && !(mountflags & MOUNT_CYGDRIVE)) root_idx = i; diff --git a/winsup/cygwin/uinfo.cc b/winsup/cygwin/uinfo.cc index 57bb6d098d..3d3b804c83 100644 --- a/winsup/cygwin/uinfo.cc +++ b/winsup/cygwin/uinfo.cc @@ -2824,7 +2824,7 @@ pwdgrp::fetch_account_from_windows (fetch_user_arg_t &arg, bool ugid_caching, cy dom, name, sid.string ((char *) sidstr), home ?: "/home/", home ? L"" : name, - shell ?: "/bin/bash"); + shell ?: "/usr/bin/bash"); if (gecos) free (gecos); if (home) From dc304d9a448fa7d31140a1f25d075abc53f710ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B8=CC=86=20=D0=9F?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=BE=D0=B2?= Date: Sun, 14 Apr 2019 21:47:21 +0300 Subject: [PATCH 036/102] Instead of creating Cygwin symlinks, use deep copy by default The new `winsymlinks` mode `deepcopy` (which is made the default) lets calls to `symlink()` create (deep) copies of the source file/directory. This is necessary because unlike Cygwin, MSYS2 does not try to be its own little ecosystem that lives its life separate from regular Win32 programs: the latter have _no idea_ about Cygwin-emulated symbolic links (i.e. system files whose contents start with `!\xff\xfe` and the remainder consists of the NUL-terminated, UTF-16LE-encoded symlink target). To support Cygwin-style symlinks, the new mode `sysfile` is introduced. Co-authored-by: Johannes Schindelin Co-authored-by: Jeremy Drake --- winsup/cygwin/environ.cc | 4 + winsup/cygwin/globals.cc | 3 +- winsup/cygwin/path.cc | 252 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 258 insertions(+), 1 deletion(-) diff --git a/winsup/cygwin/environ.cc b/winsup/cygwin/environ.cc index b9f7e05452..5fb3f53ef5 100644 --- a/winsup/cygwin/environ.cc +++ b/winsup/cygwin/environ.cc @@ -88,6 +88,10 @@ set_winsymlinks (const char *buf) else if (ascii_strncasematch (buf, "native", 6)) allow_winsymlinks = ascii_strcasematch (buf + 6, "strict") ? WSYM_nativestrict : WSYM_native; + else if (ascii_strncasematch (buf, "deepcopy", 8)) + allow_winsymlinks = WSYM_deepcopy; + else + allow_winsymlinks = WSYM_sysfile; } /* The structure below is used to set up an array which is used to diff --git a/winsup/cygwin/globals.cc b/winsup/cygwin/globals.cc index d8e058f191..b7e0e21c52 100644 --- a/winsup/cygwin/globals.cc +++ b/winsup/cygwin/globals.cc @@ -57,6 +57,7 @@ enum winsym_t WSYM_nativestrict, WSYM_nfs, WSYM_sysfile, + WSYM_deepcopy }; exit_states NO_COPY exit_state; @@ -70,7 +71,7 @@ bool ignore_case_with_glob; bool pipe_byte = true; /* Default to byte mode so that C# programs work. */ bool reset_com; bool wincmdln; -winsym_t allow_winsymlinks = WSYM_default; +winsym_t allow_winsymlinks = WSYM_deepcopy; bool disable_pcon; bool winjitdebug = false; diff --git a/winsup/cygwin/path.cc b/winsup/cygwin/path.cc index b31dfe442e..2a750bc84f 100644 --- a/winsup/cygwin/path.cc +++ b/winsup/cygwin/path.cc @@ -1722,6 +1722,173 @@ conv_path_list (const char *src, char *dst, size_t size, /********************** Symbolic Link Support **************************/ +static int +recursiveCopyCheckSymlink(PUNICODE_STRING src, bool& isdirlink) +{ + path_conv pc (src, PC_SYM_NOFOLLOW|PC_SYM_NOFOLLOW_REP); + if (pc.error) + { + set_errno (pc.error); + return -1; + } + isdirlink = pc.issymlink (); + return 0; +} + +/* + Create a deep copy of src as dst, while avoiding descending in origpath. +*/ +static int +recursiveCopy (PUNICODE_STRING src, PUNICODE_STRING dst, USHORT origsrclen, + USHORT origdstlen, PWIN32_FIND_DATAW dHfile = NULL) +{ + HANDLE dH = INVALID_HANDLE_VALUE; + NTSTATUS status; + int srcpos = src->Length; + int dstpos = dst->Length; + int res = -1; + bool freedHfile = false; + + if (!dHfile) + { + dHfile = (PWIN32_FIND_DATAW) cmalloc_abort (HEAP_STR, sizeof (*dHfile)); + freedHfile = true; + } + + debug_printf ("recursiveCopy (%S, %S)", src, dst); + + /* Create the destination directory */ + if (!CreateDirectoryExW (src->Buffer, dst->Buffer, NULL)) + { + debug_printf ("CreateDirectoryExW(%S, %S, 0) failed", src, dst); + __seterrno (); + goto done; + } + /* Descend into the source directory */ + if (src->Buffer[(src->Length - 1) / sizeof (WCHAR)] != L'\\') + { + status = RtlAppendUnicodeToString (src, L"\\*"); + } + else + { + status = RtlAppendUnicodeToString (src, L"*"); + srcpos -= sizeof (WCHAR); + } + if (!NT_SUCCESS (status)) + { + __seterrno_from_nt_status (status); + goto done; + } + if (dst->Buffer[(dst->Length - 1) / sizeof (WCHAR)] != L'\\') + status = RtlAppendUnicodeToString (dst, L"\\"); + else + dstpos -= sizeof (WCHAR); + if (!NT_SUCCESS (status)) + { + __seterrno_from_nt_status (status); + goto done; + } + + dH = FindFirstFileExW (src->Buffer, FindExInfoBasic, dHfile, + FindExSearchNameMatch, NULL, + FIND_FIRST_EX_LARGE_FETCH); + if (dH == INVALID_HANDLE_VALUE) + { + __seterrno (); + goto done; + } + + do + { + bool isdirlink = false; + debug_printf ("dHfile: %W", dHfile->cFileName); + if (dHfile->cFileName[0] == L'.' && + (!dHfile->cFileName[1] || + (dHfile->cFileName[1] == L'.' && !dHfile->cFileName[2]))) + continue; + /* Append the directory item filename to both source and destination */ + src->Length = srcpos + sizeof (WCHAR); + dst->Length = dstpos + sizeof (WCHAR); + status = RtlAppendUnicodeToString (src, dHfile->cFileName); + if (!NT_SUCCESS (status)) + { + __seterrno_from_nt_status (status); + goto done; + } + status = RtlAppendUnicodeToString (dst, dHfile->cFileName); + if (!NT_SUCCESS (status)) + { + __seterrno_from_nt_status (status); + goto done; + } + debug_printf ("%S -> %S", src, dst); + if ((dHfile->dwFileAttributes & + (FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_REPARSE_POINT)) == + (FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_REPARSE_POINT)) + { + /* I was really hoping to avoid using path_conv in the recursion, + but maybe putting it in its own function will prevent it from + taking up space in the stack frame */ + if (recursiveCopyCheckSymlink (src, isdirlink)) + goto done; + } + if (isdirlink) + { + /* CreateDirectoryEx seems to "copy" directory reparse points, which + CopyFileEx can only do with a flag introduced in 19041. */ + if (!CreateDirectoryExW (src->Buffer, dst->Buffer, NULL)) + { + debug_printf ("CreateDirectoryExW(%S, %S, 0) failed", src, dst); + __seterrno (); + goto done; + } + } + else if (dHfile->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + /* Recurse into the child directory */ + /* avoids endless recursion */ + if (src->Length <= origsrclen || + (!wcsncmp (src->Buffer, dst->Buffer, origdstlen / sizeof (WCHAR)) && + (!src->Buffer[origdstlen / sizeof (WCHAR)] || + iswdirsep(src->Buffer[origdstlen / sizeof (WCHAR)])))) + { + set_errno (ELOOP); + goto done; + } + if (recursiveCopy (src, dst, origsrclen, origdstlen, dHfile)) + goto done; + } + else + { + /* Just copy the file */ + if (!CopyFileExW (src->Buffer, dst->Buffer, NULL, NULL, NULL, + COPY_FILE_COPY_SYMLINK)) + { + __seterrno (); + goto done; + } + } + } + while (FindNextFileW (dH, dHfile)); + + if (GetLastError() != ERROR_NO_MORE_FILES) + { + __seterrno (); + goto done; + } + res = 0; + +done: + + if (dH != INVALID_HANDLE_VALUE) + FindClose (dH); + + if (freedHfile) + cfree (dHfile); + + return res; +} + /* Create a symlink from FROMPATH to TOPATH. */ extern "C" int @@ -2048,6 +2215,84 @@ symlink_wsl (const char *oldpath, path_conv &win32_newpath) return 0; } +int +symlink_deepcopy (const char *oldpath, path_conv &win32_newpath) +{ + tmp_pathbuf tp; + path_conv win32_oldpath; + + resolve_symlink_target (oldpath, win32_newpath, win32_oldpath); + if (win32_oldpath.error) + { + set_errno (win32_oldpath.error); + return -1; + } + if (win32_oldpath.isspecial ()) + return -2; + + /* MSYS copy file instead make symlink */ + /* As a MSYS limitation, the source path must exist. */ + if (!win32_oldpath.exists ()) + { + set_errno (ENOENT); + return -1; + } + + PUNICODE_STRING w_oldpath = win32_oldpath.get_nt_native_path (); + PUNICODE_STRING w_newpath = win32_newpath.get_nt_native_path (); + if (w_oldpath->Buffer[1] == L'?') + w_oldpath->Buffer[1] = L'\\'; + if (w_newpath->Buffer[1] == L'?') + w_newpath->Buffer[1] = L'\\'; + if (win32_oldpath.isdir ()) + { + /* we need a larger UNICODE_STRING MaximumLength than + get_nt_native_path allocates for the recursive copy */ + UNICODE_STRING u_oldpath, u_newpath; + RtlCopyUnicodeString (tp.u_get (&u_oldpath), w_oldpath); + RtlCopyUnicodeString (tp.u_get (&u_newpath), w_newpath); + return recursiveCopy (&u_oldpath, &u_newpath, + u_oldpath.Length, u_newpath.Length); + } + else + { + bool isdirlink = false; + if (win32_oldpath.issymlink () && + win32_oldpath.is_known_reparse_point ()) + { + /* Is there a better way to know this? */ + DWORD attr = getfileattr (win32_oldpath.get_win32 (), + !!win32_oldpath.objcaseinsensitive ()); + if (attr == INVALID_FILE_ATTRIBUTES) + { + __seterrno (); + return -1; + } + isdirlink = attr & FILE_ATTRIBUTE_DIRECTORY; + } + if (isdirlink) + { + /* CreateDirectoryEx seems to "copy" directory reparse points, which + CopyFileEx can only do with a flag introduced in 19041. */ + if (!CreateDirectoryExW (w_oldpath->Buffer, w_newpath->Buffer, NULL)) + { + debug_printf ("CreateDirectoryExW(%S, %S, 0) failed", w_oldpath, + w_newpath); + __seterrno (); + return -1; + } + } + else if (!CopyFileExW (w_oldpath->Buffer, w_newpath->Buffer, NULL, NULL, + NULL, COPY_FILE_COPY_SYMLINK)) + { + __seterrno (); + return -1; + } + } + + return 0; +} + int symlink_worker (const char *oldpath, path_conv &win32_newpath, bool isdevice) { @@ -2115,6 +2360,13 @@ symlink_worker (const char *oldpath, path_conv &win32_newpath, bool isdevice) case WSYM_nfs: res = symlink_nfs (oldpath, win32_newpath); __leave; + case WSYM_deepcopy: + res = symlink_deepcopy (oldpath, win32_newpath); + if (!res || res == -1) + __leave; + /* fall back to sysfile symlink type */ + wsym_type = WSYM_sysfile; + break; case WSYM_native: case WSYM_nativestrict: res = symlink_native (oldpath, win32_newpath); From 62649f079acb178acd7977a9b2d9d4a51b57eb54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B8=CC=86=20=D0=9F?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=BE=D0=B2?= Date: Sun, 14 Apr 2019 21:48:54 +0300 Subject: [PATCH 037/102] Automatically rewrite TERM=msys to TERM=cygwin With MSys1, it was necessary to set the TERM variable to "msys". To allow for a smooth transition from MSys1 to MSys2, let's simply handle TERM=msys as if the user had not specified TERM at all and wanted us to use our preferred TERM value. --- winsup/cygwin/environ.cc | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/winsup/cygwin/environ.cc b/winsup/cygwin/environ.cc index 5fb3f53ef5..117531367e 100644 --- a/winsup/cygwin/environ.cc +++ b/winsup/cygwin/environ.cc @@ -908,7 +908,16 @@ win32env_to_cygenv (PWCHAR rawenv, bool posify) char *eq = strchrnul (newp, '='); ucenv (newp, eq); /* uppercase env vars which need it */ if (*newp == 'T' && strncmp (newp, "TERM=", 5) == 0) - sawTERM = 1; + { + /* backwards compatibility: override TERM=msys by TERM=cygwin */ + if (strcmp (newp + 5, "msys") == 0) + { + free(newp); + i--; + continue; + } + sawTERM = 1; + } #ifdef __MSYS__ else if (*newp == 'M' && strncmp (newp, "MSYS=", 5) == 0) parse_options (newp + 5); From 3b750445eae37c0dd44991bb9125fd73e070bce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B8=CC=86=20=D0=9F?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=BE=D0=B2?= Date: Sun, 14 Apr 2019 21:50:55 +0300 Subject: [PATCH 038/102] Do not convert environment for strace Strace is a Windows program so MSYS2 will convert all arguments and environment vars and that makes debugging msys2 software with strace very tricky. --- winsup/cygwin/spawn.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/winsup/cygwin/spawn.cc b/winsup/cygwin/spawn.cc index 37137bd7a2..ac403dad42 100644 --- a/winsup/cygwin/spawn.cc +++ b/winsup/cygwin/spawn.cc @@ -543,11 +543,13 @@ child_info_spawn::worker (const char *prog_arg, const char *const *argv, bool switch_user = ::cygheap->user.issetuid () && (::cygheap->user.saved_uid != ::cygheap->user.real_uid); + bool keep_posix = (iscmd (argv[0], "strace.exe") + || iscmd (argv[0], "strace")) ? true : real_path.iscygexec (); moreinfo->envp = build_env (envp, envblock, moreinfo->envc, real_path.iscygexec (), switch_user ? ::cygheap->user.primary_token () : NULL, - real_path.iscygexec ()); + keep_posix); if (!moreinfo->envp || !envblock) { set_errno (E2BIG); From 88822d826004cd6da36411213af9ad4979d558ae Mon Sep 17 00:00:00 2001 From: Ray Donnelly Date: Sun, 23 Aug 2015 20:47:30 +0100 Subject: [PATCH 039/102] strace.cc: Don't set MSYS=noglob Commit message for this code was: * strace.cc (create_child): Set CYGWIN=noglob when starting new process so that Cygwin will leave already-parsed the command line alonw." I can see no reason for it and it badly breaks the ability to use strace.exe to investigate calling a Cygwin program from a Windows program, for example: strace mingw32-make.exe .. where mingw32-make.exe finds sh.exe and uses it as the shell. The reason it badly breaks this use-case is because dcrt0.cc depends on globbing to happen to parse commandlines from Windows programs; irrespective of whether they contain any glob patterns or not. See quoted () comment: "This must have been run from a Windows shell, so preserve quotes for globify to play with later." --- winsup/utils/mingw/strace.cc | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/winsup/utils/mingw/strace.cc b/winsup/utils/mingw/strace.cc index 29db640239..25adf4e8dd 100644 --- a/winsup/utils/mingw/strace.cc +++ b/winsup/utils/mingw/strace.cc @@ -354,10 +354,28 @@ create_child (char **argv) make_command_line (one_line, argv); SetConsoleCtrlHandler (NULL, 0); +/* Commit message for this code was: +"* strace.cc (create_child): Set CYGWIN=noglob when starting new process so that + + Cygwin will leave already-parsed the command line alonw." + + I can see no reason for it and it badly breaks the ability to use + strace.exe to investigate calling a Cygwin program from a Windows + program, for example: + strace mingw32-make.exe + .. where mingw32-make.exe finds sh.exe and uses it as the shell. + The reason it badly breaks this use-case is because dcrt0.cc depends + on globbing to happen to parse commandlines from Windows programs; + irrespective of whether they contain any glob patterns or not. + + See quoted () comment: + "This must have been run from a Windows shell, so preserve + quotes for globify to play with later." + const char *cygwin_env = getenv ("MSYS"); const char *space; - if (cygwin_env && strlen (cygwin_env) <= 256) /* sanity check */ + if (cygwin_env && strlen (cygwin_env) <= 256) // sanity check space = " "; else space = cygwin_env = ""; @@ -365,6 +383,7 @@ create_child (char **argv) + strlen (space) + strlen (cygwin_env)); sprintf (newenv, "MSYS=noglob%s%s", space, cygwin_env); _putenv (newenv); +*/ ret = CreateProcess (0, one_line.buf, /* command line */ NULL, /* Security */ NULL, /* thread */ From 41812f5d5903847fffb44eb33d287ea8b85941c3 Mon Sep 17 00:00:00 2001 From: Ray Donnelly Date: Fri, 21 Aug 2015 09:52:47 +0100 Subject: [PATCH 040/102] Add debugging for strace make_command_line --- winsup/utils/mingw/strace.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/winsup/utils/mingw/strace.cc b/winsup/utils/mingw/strace.cc index 25adf4e8dd..d346abc4e7 100644 --- a/winsup/utils/mingw/strace.cc +++ b/winsup/utils/mingw/strace.cc @@ -352,6 +352,7 @@ create_child (char **argv) flags |= CREATE_NEW_CONSOLE | CREATE_NEW_PROCESS_GROUP; make_command_line (one_line, argv); + printf ("create_child: %s\n", one_line.buf); SetConsoleCtrlHandler (NULL, 0); /* Commit message for this code was: From 8c4ab2126848ced2ca48f0c1ddb266bf0e76281f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 17 May 2017 18:13:32 +0200 Subject: [PATCH 041/102] strace --quiet: be *really* quiet The biggest problem with strace spitting out `create_child: ...` despite being asked to be real quiet is that its output can very well interfere with scripts' operations. For example, when running any of Git for Windows' shell scripts with `GIT_STRACE_COMMANDS=/path/to/logfile` (which is sadly an often needed debugging technique while trying to address the many MSYS2 issues Git for Windows faces), any time the output of any command is redirected into a variable, it will include that `create_child: ...` line, wreaking havoc with Git's expectations. So let's just really be quiet when we're asked to be quiet. Signed-off-by: Johannes Schindelin --- winsup/utils/mingw/strace.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/winsup/utils/mingw/strace.cc b/winsup/utils/mingw/strace.cc index d346abc4e7..a6b2e5d548 100644 --- a/winsup/utils/mingw/strace.cc +++ b/winsup/utils/mingw/strace.cc @@ -352,7 +352,8 @@ create_child (char **argv) flags |= CREATE_NEW_CONSOLE | CREATE_NEW_PROCESS_GROUP; make_command_line (one_line, argv); - printf ("create_child: %s\n", one_line.buf); + if (!quiet) + printf ("create_child: %s\n", one_line.buf); SetConsoleCtrlHandler (NULL, 0); /* Commit message for this code was: From f2a033d565973dac525e45187b44f8067dedaeae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B8=CC=86=20=D0=9F?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=BE=D0=B2?= Date: Sun, 14 Apr 2019 22:13:51 +0300 Subject: [PATCH 042/102] path_conv: special-case root directory to have trailing slash When converting `/c/` to `C:\`, the trailing slash is actually really necessary, as `C:` is not an absolute path. We must be very careful to do this only for root directories, though. If we kept the trailing slash also for, say, `/y/directory/`, we would run into the following issue: On FAT file systems, the normalized path is used to fake inode numbers. As a result, `Y:\directory\` and `Y:\directory` have different inode numbers!!! This would result in very non-obvious symptoms. Back when we were too careless about keeping the trailing slash, it was reported to the Git for Windows project that the `find` and `rm` commands can error out on FAT file systems with very confusing "No such file or directory" errors, for no good reason. During the original investigation, Vasil Minkov pointed out in https://github.com/git-for-windows/git/issues/1497#issuecomment-372665870, that this bug had been fixed in Cygwin as early as 1997... and the bug was unfortunately reintroduced into early MSYS2 versions. Signed-off-by: Johannes Schindelin --- winsup/cygwin/path.cc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/winsup/cygwin/path.cc b/winsup/cygwin/path.cc index 2a750bc84f..4a9f7b9cba 100644 --- a/winsup/cygwin/path.cc +++ b/winsup/cygwin/path.cc @@ -742,6 +742,12 @@ path_conv::check (const char *src, unsigned opt, need_directory = 1; *--tail = '\0'; } + /* Special case for "/" must set need_directory, without removing + trailing slash */ + else if (tail == path_copy + 1 && isslash (tail[-1])) + { + need_directory = 1; + } path_end = tail; /* Scan path_copy from right to left looking either for a symlink @@ -1288,6 +1294,7 @@ path_conv::check (const char *src, unsigned opt, cfree (wide_path); wide_path = NULL; } + if (need_directory) { size_t n = strlen (this->path); From f5ab94dff17fb1827123c985f0d7e163a0e3d014 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 8 Nov 2022 16:24:20 +0100 Subject: [PATCH 043/102] When converting to a Unix path, avoid double trailing slashes When calling `cygpath -u C:/msys64/` in an MSYS2 setup that was installed into `C:/msys64/`, the result should be `/`, not `//`. Let's ensure that we do not append another trailing slash if the converted path already ends in a slash. This fixes https://github.com/msys2/msys2-runtime/issues/112 Signed-off-by: Johannes Schindelin --- winsup/cygwin/mount.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/winsup/cygwin/mount.cc b/winsup/cygwin/mount.cc index affb7e9266..ff0279336b 100644 --- a/winsup/cygwin/mount.cc +++ b/winsup/cygwin/mount.cc @@ -1018,6 +1018,9 @@ mount_info::conv_to_posix_path (const char *src_path, char *posix_path, nextchar = 1; int addslash = nextchar > 0 ? 1 : 0; + /* avoid appending a slash if the result already has a trailing slash */ + if (append_slash && mi.posix_pathlen && mi.posix_path[mi.posix_pathlen-1] == '/') + append_slash = addslash = 0; if ((mi.posix_pathlen + (pathbuflen - mi.native_pathlen) + addslash) >= NT_MAX_PATH) return ENAMETOOLONG; strcpy (posix_path, mi.posix_path); From 6c551e9c8e700a18dee2babc6ffa41c2a83c98e1 Mon Sep 17 00:00:00 2001 From: Ray Donnelly Date: Fri, 21 Aug 2015 12:52:09 +0100 Subject: [PATCH 044/102] dcrt0.cc: Untangle allow_glob from winshell Otherwise if globbing is allowed and we get called from a Windows program, build_argv thinks we've been called from a Cygwin program. --- winsup/cygwin/dcrt0.cc | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/winsup/cygwin/dcrt0.cc b/winsup/cygwin/dcrt0.cc index e19b7d3904..8fc3672d2d 100644 --- a/winsup/cygwin/dcrt0.cc +++ b/winsup/cygwin/dcrt0.cc @@ -154,12 +154,12 @@ isquote (char c) /* Step over a run of characters delimited by quotes */ static /*__inline*/ char * -quoted (char *cmd, int winshell) +quoted (char *cmd, int winshell, int glob) { char *p; char quote = *cmd; - if (!winshell) + if (!winshell || !glob) { char *p; strcpy (cmd, cmd + 1); @@ -169,8 +169,8 @@ quoted (char *cmd, int winshell) } const char *s = quote == '\'' ? "'" : "\\\""; - /* This must have been run from a Windows shell, so preserve - quotes for globify to play with later. */ + /* This must have been run from a Windows shell and globbing is enabled, + so preserve quotes for globify to play with later. */ while (*cmd && *++cmd) if ((p = strpbrk (cmd, s)) == NULL) { @@ -292,7 +292,7 @@ globify (char *word, char **&argv, int &argc, int &argvlen) /* Build argv, argc from string passed from Windows. */ static void -build_argv (char *cmd, char **&argv, int &argc, int winshell) +build_argv (char *cmd, char **&argv, int &argc, int winshell, int glob) { int argvlen = 0; int nesting = 0; // monitor "nesting" from insert_file @@ -326,7 +326,7 @@ build_argv (char *cmd, char **&argv, int &argc, int winshell) a Cygwin process, or if the word starts with a '@'. In this case, the insert_file function needs an unquoted DOS filename and globbing isn't performed anyway. */ - cmd = quoted (cmd, winshell && argc > 0 && *word != '@'); + cmd = quoted (cmd, winshell && argc > 0 && *word != '@', glob); } if (issep (*cmd)) // End of argument if space break; @@ -352,7 +352,7 @@ build_argv (char *cmd, char **&argv, int &argc, int winshell) } /* Add word to argv file after (optional) wildcard expansion. */ - if (!winshell || !argc || !globify (word, argv, argc, argvlen)) + if (!glob || !argc || !globify (word, argv, argc, argvlen)) { debug_printf ("argv[%d] = '%s'", argc, word); argv[argc++] = word; @@ -907,6 +907,7 @@ dll_crt0_1 (void *) /* Scan the command line and build argv. Expand wildcards if not called from another cygwin process. */ build_argv (line, __argv, __argc, + NOTSTATE (myself, PID_CYGPARENT), NOTSTATE (myself, PID_CYGPARENT) && allow_glob); /* Convert argv[0] to posix rules if it's currently blatantly From 145bb5c8968a746b88acdd3288786ff62a3dde5f Mon Sep 17 00:00:00 2001 From: Ray Donnelly Date: Mon, 24 Aug 2015 00:48:06 +0100 Subject: [PATCH 045/102] dcrt0.cc (globify): Don't quote literal strings differently when dos_spec Reverts 25ba8f306f3099caf8397859019e936b90510e8d. I can't figure out what the intention was. I'm sure I'll find out soon enough when everything breaks. This change means that input of: '"C:/test.exe SOME_VAR=\"literal quotes\""' becomes: 'C:/test.exe SOME_VAR="literal quotes"' instead of: 'C:/test.exe SOME_VAR=\literal quotes\' .. which is at least consistent with the result for: '"no_drive_or_colon SOME_VAR=\"literal quotes\""' The old result of course resulted in the quoted string being split into two arguments at the space which is clearly not intended. I *guess* backslashes in dos paths may have been the issue here? If so I don't care since we should not use them, ever, esp. not at the expense of sensible forward-slash-containing input. --- winsup/cygwin/dcrt0.cc | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/winsup/cygwin/dcrt0.cc b/winsup/cygwin/dcrt0.cc index 8fc3672d2d..3a2d0ec651 100644 --- a/winsup/cygwin/dcrt0.cc +++ b/winsup/cygwin/dcrt0.cc @@ -237,10 +237,20 @@ globify (char *word, char **&argv, int &argc, int &argvlen) while (*++s && *s != quote) { mbstate_t mbs = { 0 }; + /* This used to be: if (dos_spec || *s != '\\') - /* nothing */; + // nothing else if (s[1] == quote || s[1] == '\\') s++; + With commit message: + dcrt0.cc (globify): Don't use \ quoting when apparently quoting a DOS path + spec, even within a quoted string. + But that breaks the "literal quotes" part of '"C:/test.exe SOME_VAR=\"literal quotes\""' + giving: 'C:/test.exe SOME_VAR=\literal quotes\' (with \'s between each character) + instead of 'C:/test.exe SOME_VAR="literal quotes"' (with \'s between each character) + */ + if (*s == '\\' && (s[1] == quote || s[1] == '\\')) + s++; *p++ = '\\'; size_t cnt = isascii (*s) ? 1 : mbrtowi (NULL, s, MB_CUR_MAX, &mbs); if (cnt <= 1 || cnt == (size_t)-1) From 8949d0dc3c3ee6e0af9d5a53bbf9e065af0d8f9f Mon Sep 17 00:00:00 2001 From: Ray Donnelly Date: Fri, 21 Aug 2015 12:18:52 +0100 Subject: [PATCH 046/102] Add debugging for build_argv --- winsup/cygwin/dcrt0.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/winsup/cygwin/dcrt0.cc b/winsup/cygwin/dcrt0.cc index 3a2d0ec651..4d622cdc28 100644 --- a/winsup/cygwin/dcrt0.cc +++ b/winsup/cygwin/dcrt0.cc @@ -311,6 +311,8 @@ build_argv (char *cmd, char **&argv, int &argc, int winshell, int glob) argvlen = 0; argv = NULL; + debug_printf ("cmd = '%s', winshell = %d, glob = %d", cmd, winshell, glob); + /* Scan command line until there is nothing left. */ while (*cmd) { From c78d8a53bbb9b9744bb3e473c14fc1865fb5bca1 Mon Sep 17 00:00:00 2001 From: Ray Donnelly Date: Sun, 10 Apr 2016 21:47:41 +0100 Subject: [PATCH 047/102] environ.cc: New facility/environment variable MSYS2_ENV_CONV_EXCL Works very much like MSYS2_ARG_CONV_EXCL. In fact it uses the same function, arg_heuristic_with_exclusions (). Also refactors parsing the env. variables to use new function, string_split_delimited (). The env. that is searched through is the merged (POSIX + Windows) one. It remains to be seen if this should be made an option or not. This feature was prompted because the R language (Windows exe) calls bash to run configure.win, which then calls back into R to read its config variables (LOCAL_SOFT) and when this happens, msys2-runtime converts R_ARCH from "/x64" to an absolute Windows path and appends it to another absolute path, R_HOME, forming an invalid path. --- winsup/cygwin/environ.cc | 34 +++++++++++++++++------- winsup/cygwin/local_includes/miscfuncs.h | 2 ++ winsup/cygwin/miscfuncs.cc | 20 ++++++++++++++ winsup/cygwin/path.cc | 1 - winsup/cygwin/spawn.cc | 12 ++------- 5 files changed, 48 insertions(+), 21 deletions(-) diff --git a/winsup/cygwin/environ.cc b/winsup/cygwin/environ.cc index 117531367e..a9cce9645a 100644 --- a/winsup/cygwin/environ.cc +++ b/winsup/cygwin/environ.cc @@ -1173,6 +1173,10 @@ build_env (const char * const *envp, PWCHAR &envblock, int &envc, int tl = 0; char **pass_dstp; +#ifdef __MSYS__ + char *msys2_env_conv_excl_env = NULL; + size_t msys2_env_conv_excl_count = 0; +#endif char **pass_env = (char **) alloca (sizeof (char *) * (n + winnum + SPENVS_SIZE + 1)); /* Iterate over input list, generating a new environment list and refreshing @@ -1181,16 +1185,25 @@ build_env (const char * const *envp, PWCHAR &envblock, int &envc, { bool calc_tl = !no_envblock; #ifdef __MSYS__ - /* Don't pass timezone environment to non-msys applications */ - if (!keep_posix && ascii_strncasematch(*srcp, "TZ=", 3)) + if (!keep_posix) { - const char *v = *srcp + 3; - if (*v == ':') - goto next1; - for (; *v; v++) - if (!isalpha(*v) && !isdigit(*v) && - *v != '-' && *v != '+' && *v != ':') - goto next1; + /* Don't pass timezone environment to non-msys applications */ + if (ascii_strncasematch(*srcp, "TZ=", 3)) + { + const char *v = *srcp + 3; + if (*v == ':') + goto next1; + for (; *v; v++) + if (!isalpha(*v) && !isdigit(*v) && + *v != '-' && *v != '+' && *v != ':') + goto next1; + } + else if (ascii_strncasematch(*srcp, "MSYS2_ENV_CONV_EXCL=", 20)) + { + msys2_env_conv_excl_env = (char*)alloca (strlen(&(*srcp)[20])+1); + strcpy (msys2_env_conv_excl_env, &(*srcp)[20]); + msys2_env_conv_excl_count = string_split_delimited (msys2_env_conv_excl_env, ';'); + } } #endif /* Look for entries that require special attention */ @@ -1315,7 +1328,8 @@ build_env (const char * const *envp, PWCHAR &envblock, int &envc, } #ifdef __MSYS__ else if (!keep_posix) { - char *win_arg = arg_heuristic(*srcp); + char *win_arg = arg_heuristic_with_exclusions + (*srcp, msys2_env_conv_excl_env, msys2_env_conv_excl_count); debug_printf("WIN32_PATH is %s", win_arg); p = cstrdup1(win_arg); if (win_arg != *srcp) diff --git a/winsup/cygwin/local_includes/miscfuncs.h b/winsup/cygwin/local_includes/miscfuncs.h index fd10e40f13..1f2627fb2d 100644 --- a/winsup/cygwin/local_includes/miscfuncs.h +++ b/winsup/cygwin/local_includes/miscfuncs.h @@ -84,6 +84,8 @@ void backslashify (const char *, char *, bool); void slashify (const char *, char *, bool); #define isslash(c) ((c) == '/') +size_t string_split_delimited (char * string, char delimiter); + extern void transform_chars (PWCHAR, PWCHAR); extern inline void transform_chars (PUNICODE_STRING upath, USHORT start_idx) diff --git a/winsup/cygwin/miscfuncs.cc b/winsup/cygwin/miscfuncs.cc index 31080d043a..f3bfba0e44 100644 --- a/winsup/cygwin/miscfuncs.cc +++ b/winsup/cygwin/miscfuncs.cc @@ -424,6 +424,26 @@ NT_readline::gets () } } +/* Searches through string for delimiter replacing each instance with '\0' + and returning the number of such delimited substrings. This function + Will return 0 for the NULL string and at least 1 otherwise. */ + +size_t +string_split_delimited (char * string, char delimiter) +{ + if ( string == NULL ) + return 0; + size_t count = 1; + string = strchr ( string, delimiter ); + while (string) + { + *string = '\0'; + ++count; + string = strchr ( string + 1, delimiter ); + } + return count; +} + /* Signal the thread name to any attached debugger (See "How to: Set a Thread Name in Native Code" diff --git a/winsup/cygwin/path.cc b/winsup/cygwin/path.cc index 4a9f7b9cba..aed115792c 100644 --- a/winsup/cygwin/path.cc +++ b/winsup/cygwin/path.cc @@ -4177,7 +4177,6 @@ arg_heuristic_with_exclusions (char const * const arg, char const * exclusions, return arg_result; } - debug_printf("Input value: (%s)", arg); for (size_t excl = 0; excl < exclusions_count; ++excl) { /* Since we've got regex linked we should maybe switch to that, but diff --git a/winsup/cygwin/spawn.cc b/winsup/cygwin/spawn.cc index ac403dad42..9408903659 100644 --- a/winsup/cygwin/spawn.cc +++ b/winsup/cygwin/spawn.cc @@ -287,8 +287,7 @@ child_info_spawn::worker (const char *prog_arg, const char *const *argv, int res = -1; /* Environment variable MSYS2_ARG_CONV_EXCL contains a list - of ';' separated argument prefixes to pass un-modified.. - It isn't applied to env. variables; only spawn arguments. + of ';' separated argument prefixes to pass un-modified. A value of * means don't convert any arguments. */ char* msys2_arg_conv_excl_env = getenv("MSYS2_ARG_CONV_EXCL"); char* msys2_arg_conv_excl = NULL; @@ -297,14 +296,7 @@ child_info_spawn::worker (const char *prog_arg, const char *const *argv, { msys2_arg_conv_excl = (char*)alloca (strlen(msys2_arg_conv_excl_env)+1); strcpy (msys2_arg_conv_excl, msys2_arg_conv_excl_env); - msys2_arg_conv_excl_count = 1; - msys2_arg_conv_excl_env = strchr ( msys2_arg_conv_excl, ';' ); - while (msys2_arg_conv_excl_env) - { - *msys2_arg_conv_excl_env = '\0'; - ++msys2_arg_conv_excl_count; - msys2_arg_conv_excl_env = strchr ( msys2_arg_conv_excl_env + 1, ';' ); - } + msys2_arg_conv_excl_count = string_split_delimited (msys2_arg_conv_excl, ';'); } /* Check if we have been called from exec{lv}p or spawn{lv}p and mask From 6673896216c16c1c20bf13160436641480e3e05a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 19 May 2020 13:49:37 +0200 Subject: [PATCH 048/102] Introduce the `enable_pcon` value for `MSYS` It is simply the negation of `disable_pcon`, i.e. `MSYS=enable_pcon` is equivalent to `MSYS=nodisable_pcon` (the former is slightly more intuitive than the latter) and likewise `MSYS=noenable_pcon` is equivalent to `MSYS=disable_pcon` (here, the latter is definitely more intuitive than the former). This is needed because we just demoted the pseudo console feature to be opt-in instead of opt-out, and it would be awkward to recommend to users to use "nodisable_pcon"... "nodisable" is not even a verb. Signed-off-by: Johannes Schindelin --- winsup/cygwin/environ.cc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/winsup/cygwin/environ.cc b/winsup/cygwin/environ.cc index a9cce9645a..b9600ef9a8 100644 --- a/winsup/cygwin/environ.cc +++ b/winsup/cygwin/environ.cc @@ -42,6 +42,7 @@ enum settings isfunc, setdword, setbool, + setnegbool, setbit }; @@ -118,6 +119,7 @@ static struct parse_thing } known[] NO_COPY = { {"disable_pcon", {&disable_pcon}, setbool, NULL, {{false}, {true}}}, + {"enable_pcon", {&disable_pcon}, setnegbool, NULL, {{true}, {false}}}, {"error_start", {func: error_start_init}, isfunc, NULL, {{0}, {0}}}, {"export", {&export_settings}, setbool, NULL, {{false}, {true}}}, {"glob", {func: glob_init}, isfunc, NULL, {{0}, {s: "normal"}}}, @@ -244,6 +246,13 @@ parse_options (const char *inbuf) *k->setting.b = !!strtol (eq, NULL, 0); debug_printf ("%s%s", *k->setting.b ? "" : "no", k->name); break; + case setnegbool: + if (!istrue || !eq) + *k->setting.b = k->values[istrue].i; + else + *k->setting.b = !strtol (eq, NULL, 0); + debug_printf ("%s%s", !*k->setting.b ? "" : "no", k->name); + break; case setbit: *k->setting.x &= ~k->values[istrue].i; if (istrue || (eq && strtol (eq, NULL, 0))) From 8df76a0f20dcf9e6a297a8c928c7878686b11fc5 Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Fri, 5 Jun 2020 20:09:11 +0200 Subject: [PATCH 049/102] popen: call /usr/bin/sh instead of /bin/sh We mount /usr/bin to /bin, but in a chroot this is broken and we have no /bin, so try to use the real path. chroot is used by pacman to run install scripts when called with --root and this broke programs in install scripts calling popen() (install-info from texinfo for example) There are more paths hardcoded to /bin in cygwin which might also be broken in this scenario, so this maybe should be extended to all of them. --- winsup/cygwin/syscalls.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winsup/cygwin/syscalls.cc b/winsup/cygwin/syscalls.cc index 12b7a4f2fb..3ad2039321 100644 --- a/winsup/cygwin/syscalls.cc +++ b/winsup/cygwin/syscalls.cc @@ -4538,7 +4538,7 @@ popen (const char *command, const char *in_type) /* Start a shell process to run the given command without forking. */ child_info_spawn ch_spawn_local (_CH_NADA); - pid_t pid = ch_spawn_local.worker ("/bin/sh", argv, environ, _P_NOWAIT, + pid_t pid = ch_spawn_local.worker ("/usr/bin/sh", argv, environ, _P_NOWAIT, __std[0], __std[1]); /* Reinstate the close-on-exec state */ From bb5abd5db3647e58f6a4433bea5c4ae1dbf382ea Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 17 Mar 2021 17:41:02 +0100 Subject: [PATCH 050/102] Disable the 'cygwin' GitHub workflow It does not work at all. For example, `rpm -E %fedora` says that there should be version 33 of rpmsphere at https://github.com/rpmsphere/noarch/tree/master/r, but there is only version 32. Another thing that is broken: Cygwin now assumes that a recent mingw-w64-headers version is available, but Fedora apparently only offers v7.0.0, which is definitely too old to accommodate for the expectation of https://github.com/cygwin/cygwin/commit/c1f7c4d1b6d7. Signed-off-by: Johannes Schindelin --- .github/workflows/cygwin.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/cygwin.yml b/.github/workflows/cygwin.yml index 877f54cdeb..998dc01576 100644 --- a/.github/workflows/cygwin.yml +++ b/.github/workflows/cygwin.yml @@ -1,12 +1,6 @@ name: cygwin -on: - push: - # since master is a symbolic reference to main, don't run for both - branches-ignore: - - 'master' - tags: - - '*' +on: workflow_dispatch jobs: fedora-build: From 6e6dac81d503ca1db36d54cffba6312ad6002b46 Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Sun, 9 Aug 2020 14:02:51 +0200 Subject: [PATCH 051/102] CI: add a GHA for doing a basic build test Build with --disable-dependency-tracking because we only build once and this saves 3-4 minutes in CI. --- .github/workflows/build.yaml | 95 ++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 .github/workflows/build.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000000..1057c35bea --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,95 @@ +name: build + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + build: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: setup-msys2 + uses: msys2/setup-msys2@v2 + with: + msystem: MSYS + update: true + install: msys2-devel base-devel autotools cocom diffutils gcc gettext-devel libiconv-devel make mingw-w64-cross-crt mingw-w64-cross-gcc mingw-w64-cross-zlib perl zlib-devel xmlto docbook-xsl libzstd-devel + + - name: Build + shell: msys2 {0} + run: | + (cd winsup && ./autogen.sh) + ./configure --disable-dependency-tracking --with-msys2-runtime-commit="$GITHUB_SHA" + make -j8 + + - name: Install + shell: msys2 {0} + run: | + make DESTDIR="$(pwd)"/_dest install + + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: install + path: _dest/ + + generate-msys2-tests-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + steps: + - id: matrix + uses: msys2/msys2-tests/gha-matrix-gen@main + + msys2-tests: + needs: [build, generate-msys2-tests-matrix] + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.generate-msys2-tests-matrix.outputs.matrix) }} + + name: msys2-tests ${{ matrix.msystem }}-${{ matrix.cc }} + runs-on: ${{ matrix.runner }} + env: + CC: ${{ matrix.cc }} + CXX: ${{ matrix.cxx }} + FC: ${{ matrix.fc }} + steps: + - id: msys2 + uses: msys2/setup-msys2@v2 + with: + msystem: ${{ matrix.msystem }} + update: true + install: ${{ matrix.packages }} + + - name: Add staging repo + shell: msys2 {0} + run: | + sed -i '1s|^|[staging]\nServer = https://repo.msys2.org/staging/\nSigLevel = Never\n|' /etc/pacman.conf + + - name: Update using staging + shell: pwsh + run: | + msys2 -c 'pacman --noconfirm -Suuy' + $ErrorActionPreference = 'Stop' + $PSNativeCommandUseErrorActionPreference = $true + msys2 -c 'pacman --noconfirm -Suu' + + - name: Download msys2-runtime artifact + uses: actions/download-artifact@v4 + with: + name: install + path: ${{ steps.msys2.outputs.msys2-location }} + + - name: uname -a + shell: msys2 {0} + run: uname -a + + - name: Run tests + uses: msys2/msys2-tests@main + From 328037015f610ce2adcfbf299d0a34b9225e6edc Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 22 Nov 2019 11:20:22 +0100 Subject: [PATCH 052/102] Set up a GitHub Action to keep in sync with Cygwin This will help us by automating an otherwise tedious task. Signed-off-by: Johannes Schindelin --- .github/workflows/sync-with-cygwin.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/sync-with-cygwin.yml diff --git a/.github/workflows/sync-with-cygwin.yml b/.github/workflows/sync-with-cygwin.yml new file mode 100644 index 0000000000..57bd30e5da --- /dev/null +++ b/.github/workflows/sync-with-cygwin.yml @@ -0,0 +1,24 @@ +name: sync-with-cygwin + +# File: .github/workflows/repo-sync.yml + +on: + workflow_dispatch: + schedule: + - cron: "42 * * * *" +jobs: + repo-sync: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Fetch Cygwin's latest master and tags + run: | + git init --bare + # Potentially use git://sourceware.org/git/newlib-cygwin.git directly, but GitHub seems more reliable + git fetch https://github.com/cygwin/cygwin master:refs/heads/cygwin/master 'refs/tags/*:refs/tags/*' + - name: Push to our fork + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git push https://$GITHUB_ACTOR:$GITHUB_TOKEN@github.com/$GITHUB_REPOSITORY refs/heads/cygwin/master 'refs/tags/*:refs/tags/*' From cf378f27c42c795e276ed22eed24b2bda77ce285 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 12 Aug 2020 12:22:38 +0200 Subject: [PATCH 053/102] Expose full command-lines to other Win32 processes by default In the Cygwin project, it was decided that the command-line of Cygwin processes, as shown in the output of `wmic process list`, would suffer from being truncated to 32k (and is transmitted to the child process via a different mechanism, anyway), and therefore only the absolute path of the executable is shown by default. Users who would like to see the full command-line (even if it is truncated) are expected to set `CYGWIN=wincmdln` (or, in MSYS2's case, `MSYS=wincmdln`). Seeing as MSYS2 tries to integrate much better with the surrounding Win32 ecosystem than Cygwin, it makes sense to turn this on by default. Users who wish to suppress it can still set `MSYS=nowincmdln`. Signed-off-by: Johannes Schindelin --- winsup/cygwin/globals.cc | 2 +- winsup/doc/cygwinenv.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/winsup/cygwin/globals.cc b/winsup/cygwin/globals.cc index b7e0e21c52..79f9476330 100644 --- a/winsup/cygwin/globals.cc +++ b/winsup/cygwin/globals.cc @@ -70,7 +70,7 @@ bool allow_glob = true; bool ignore_case_with_glob; bool pipe_byte = true; /* Default to byte mode so that C# programs work. */ bool reset_com; -bool wincmdln; +bool wincmdln = true; winsym_t allow_winsymlinks = WSYM_deepcopy; bool disable_pcon; bool winjitdebug = false; diff --git a/winsup/doc/cygwinenv.xml b/winsup/doc/cygwinenv.xml index fcb6e22485..4ea63b407a 100644 --- a/winsup/doc/cygwinenv.xml +++ b/winsup/doc/cygwinenv.xml @@ -90,7 +90,7 @@ time and when handles are inherited. Defaults to set. (no)wincmdln - if set, the windows complete command line (truncated to ~32K) will be passed on any processes that it creates -in addition to the normal UNIX argv list. Defaults to not set. +in addition to the normal UNIX argv list. Defaults to set. From 75ea5cb3e60cf3ca93c6871e069c068bdd979747 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 16 Apr 2018 14:59:39 +0200 Subject: [PATCH 054/102] Add a helper to obtain a function's address in kernel32.dll In particular, we are interested in the address of the CtrlRoutine and the ExitProcess functions. Since kernel32.dll is loaded first thing, the addresses will be the same for all processes (matching the CPU architecture, of course). This will help us with emulating SIGINT properly (by not sending signals to *all* processes attached to the same Console, as GenerateConsoleCtrlEvent() would do). Co-authored-by: Naveen M K Signed-off-by: Johannes Schindelin --- winsup/configure.ac | 5 + winsup/utils/mingw/Makefile.am | 15 ++ winsup/utils/mingw/getprocaddr.c | 310 +++++++++++++++++++++++++++++++ 3 files changed, 330 insertions(+) create mode 100644 winsup/utils/mingw/getprocaddr.c diff --git a/winsup/configure.ac b/winsup/configure.ac index 9b9b59dbcb..b9e3977fcf 100644 --- a/winsup/configure.ac +++ b/winsup/configure.ac @@ -106,6 +106,11 @@ if test "x$with_cross_bootstrap" != "xyes"; then test -n "$MINGW_CXX" || AC_MSG_ERROR([no acceptable MinGW g++ found in \$PATH]) AC_CHECK_PROGS(MINGW_CC, ${target_cpu}-w64-mingw32-gcc) test -n "$MINGW_CC" || AC_MSG_ERROR([no acceptable MinGW gcc found in \$PATH]) + + AC_CHECK_PROGS(MINGW32_CC, i686-w64-mingw32-gcc) + test -n "$MINGW32_CC" || AC_MSG_ERROR([no acceptable mingw32 gcc found in \$PATH]) + AC_CHECK_PROGS(MINGW64_CC, x86_64-w64-mingw32-gcc) + test -n "$MINGW64_CC" || AC_MSG_ERROR([no acceptable mingw64 gcc found in \$PATH]) fi AM_CONDITIONAL(CROSS_BOOTSTRAP, [test "x$with_cross_bootstrap" != "xyes"]) diff --git a/winsup/utils/mingw/Makefile.am b/winsup/utils/mingw/Makefile.am index 7f7317ae15..07b9f928d4 100644 --- a/winsup/utils/mingw/Makefile.am +++ b/winsup/utils/mingw/Makefile.am @@ -26,6 +26,21 @@ bin_PROGRAMS = \ ldh \ strace +libexec_PROGRAMS = getprocaddr32 getprocaddr64 + +# Must *not* use -O2 here, as it screws up the stack backtrace +getprocaddr32.o: %32.o: %.c + $(MINGW32_CC) -c -o $@ $< + +getprocaddr32.exe: %.exe: %.o + $(MINGW32_CC) -o $@ $^ -static -ldbghelp + +getprocaddr64.o: %64.o: %.c + $(MINGW64_CC) -c -o $@ $< + +getprocaddr64.exe: %.exe: %.o + $(MINGW64_CC) -o $@ $^ -static -ldbghelp + cygcheck_SOURCES = \ bloda.cc \ cygcheck.cc \ diff --git a/winsup/utils/mingw/getprocaddr.c b/winsup/utils/mingw/getprocaddr.c new file mode 100644 index 0000000000..25814c7bdd --- /dev/null +++ b/winsup/utils/mingw/getprocaddr.c @@ -0,0 +1,310 @@ +/* getprocaddr.c + +This program is a helper for getting the pointers for the +functions in kernel32 module, and optionally injects a remote +thread that runs those functions given a pid and exit code. + +We use dbghelp.dll to get the pointer to kernel32!CtrlRoutine +because it isn't exported. For that, we try to generate console +event (Ctrl+Break) ourselves, to find the pointer, and it is +printed if asked to, or a remote thread is injected to run the +given function. + +This software is a copyrighted work licensed under the terms of the +Cygwin license. Please consult the file "CYGWIN_LICENSE" for +details. */ + +#include +#include + +/* Include dbghelp.h after windows.h */ +#include + +static DWORD pid; +static uintptr_t exit_code; +static HANDLE CtrlEvent; + +static int +inject_remote_thread_into_process (HANDLE process, + LPTHREAD_START_ROUTINE address, + uintptr_t exit_code, + DWORD *thread_return) +{ + int res = -1; + + if (!address) + return res; + DWORD thread_id; + HANDLE thread = CreateRemoteThread (process, NULL, 1024 * 1024, address, + (PVOID)exit_code, 0, &thread_id); + if (thread) + { + /* + * Wait up to 10 seconds (arbitrary constant) for the thread to finish; + * Maybe we should wait forever? I have seen Cmd does so, but well... + */ + if (WaitForSingleObject (thread, 10000) == WAIT_OBJECT_0) + res = 0; + /* + According to the docs at MSDN for GetExitCodeThread, it will + get the return value from the function, here CtrlRoutine. So, this + checks if the Ctrl Event is handled correctly by the process. + + By some testing I could see CtrlRoutine returns 0 in case where + CtrlEvent set by SetConsoleCtrlHandler is handled correctly, in all + other cases it returns something non-zero(not sure what it that). + */ + if (thread_return != NULL) + GetExitCodeThread (thread, thread_return); + + CloseHandle (thread); + } + + return res; +} + +/* Here, we send a CtrlEvent to the current process for the + * sole purpose of capturing the address of the CtrlRoutine + * function, by looking the stack trace. + * + * This hack is needed because we cannot use GetProcAddress() + * as we do for ExitProcess(), because CtrlRoutine is not + * exported (although the .pdb files ensure that we can see + * it in a debugger). + */ +static WINAPI BOOL +ctrl_handler (DWORD ctrl_type) +{ + unsigned short count; + void *address; + HANDLE process; + PSYMBOL_INFOW info; + DWORD64 displacement; + DWORD thread_return = 0; + + count = CaptureStackBackTrace (1l /* skip this function */, + 1l /* return only one trace item */, &address, + NULL); + if (count != 1) + { + fprintf (stderr, "Could not capture backtrace\n"); + return FALSE; + } + + process = GetCurrentProcess (); + if (!SymInitialize (process, NULL, TRUE)) + { + fprintf (stderr, "Could not initialize symbols\n"); + return FALSE; + } + + info = (PSYMBOL_INFOW)malloc (sizeof (*info) + + MAX_SYM_NAME * sizeof (wchar_t)); + if (!info) + { + fprintf (stderr, "Could not allocate symbol info structure\n"); + return FALSE; + } + info->SizeOfStruct = sizeof (*info); + info->MaxNameLen = MAX_SYM_NAME; + + if (!SymFromAddrW (process, (DWORD64) (intptr_t)address, &displacement, + info)) + { + fprintf (stderr, "Could not get symbol info\n"); + SymCleanup (process); + return FALSE; + } + + if (pid == 0) + { + printf ("%p\n", (void *)(intptr_t)info->Address); + } + else + { + LPTHREAD_START_ROUTINE address = + (LPTHREAD_START_ROUTINE) (intptr_t)info->Address; + HANDLE h = OpenProcess (PROCESS_CREATE_THREAD | + PROCESS_QUERY_INFORMATION | + PROCESS_VM_OPERATION | + PROCESS_VM_WRITE | + PROCESS_VM_READ, FALSE, pid); + if (h == NULL) + { + fprintf (stderr, "OpenProcess failed: %ld\n", GetLastError ()); + return 1; + } + /* Inject the remote thread only when asked to */ + if (inject_remote_thread_into_process (h, address, exit_code, + &thread_return) < 0) + { + fprintf (stderr, + "Error while injecting remote thread for pid(%lu)\n", pid); + exit (1); /*We should exit immediately or else there will a 10s hang + waiting for the event to happen.*/ + } + if (thread_return) + fprintf (stderr, + "Injected remote thread for pid(%lu) returned %lu\n", pid, + thread_return); + } + SymCleanup (process); + if (!SetEvent (CtrlEvent)) + { + fprintf (stderr, "SetEvent failed (%ld)\n", GetLastError ()); + return 1; + } + exit (thread_return != 0); +} + +/* The easy route for finding the address of CtrlRoutine + * would be use GetProcAddress() but this isn't viable + * here because that symbol isn't exported. + */ +static int +find_ctrl_routine_the_hard_way () +{ + /* + * Avoid terminating all processes attached to the current console; + * This would happen if we used the same console as the caller, though, + * because we are sending a CtrlEvent on purpose (which _is_ sent to + * all processes connected to the same console, and the other processes + * are most likely unprepared for that CTRL_BREAK_EVENT and would be + * terminated as a consequence, _including the caller_). + * + * In case we get only one result from GetConsoleProcessList(), we don't + * need to create and allocate a new console, and it could avoid a console + * window popping up. + */ + DWORD proc_lists; + if (GetConsoleProcessList (&proc_lists, 5) > 1) + { + if (!FreeConsole () && GetLastError () != ERROR_INVALID_PARAMETER) + { + fprintf (stderr, "Could not detach from current Console: %ld\n", + GetLastError ()); + return 1; + } + if (!AllocConsole ()) + { + fprintf (stderr, "Could not allocate a new Console\n"); + return 1; + } + } + + CtrlEvent = CreateEvent (NULL, // default security attributes + TRUE, // manual-reset event + FALSE, // initial state is nonsignaled + NULL // object name + ); + + if (CtrlEvent == NULL) + { + fprintf (stderr, "CreateEvent failed (%ld)\n", GetLastError ()); + return 1; + } + + + if (!SetConsoleCtrlHandler (ctrl_handler, TRUE)) + { + fprintf (stderr, "Could not register Ctrl handler\n"); + return 1; + } + + if (!GenerateConsoleCtrlEvent (CTRL_BREAK_EVENT, 0)) + { + fprintf (stderr, "Could not simulate Ctrl+Break\n"); + return 1; + } + + if (WaitForSingleObject (CtrlEvent, 10000 /* 10 seconds*/) != WAIT_OBJECT_0) + { + fprintf (stderr, "WaitForSingleObject failed (%ld)\n", GetLastError ()); + return 1; + } + return 0; +} + +static void * +get_proc_addr (const char * module_name, const char * function_name) +{ + HMODULE module = GetModuleHandle (module_name); + if (!module) + return NULL; + return (void *)GetProcAddress (module, function_name); +} + +int +main (int argc, char **argv) +{ + char *end; + void *address; + BOOL is_ctrl_routine; + DWORD thread_return = 0; + + if (argc == 4) + { + exit_code = atoi (argv[2]); + pid = strtoul (argv[3], NULL, 0); + } + else if (argc == 2) + { + pid = 0; + } + else + { + fprintf (stderr, "Need a function name, exit code and pid\n" + "Or needs a function name.\n"); + return 1; + } + + is_ctrl_routine = strcmp (argv[1], "CtrlRoutine") == 0; + address = get_proc_addr ("kernel32", argv[1]); + if (is_ctrl_routine && !address) + { + /* CtrlRoutine is undocumented, and has been seen in both + * kernel32 and kernelbase + */ + address = get_proc_addr ("kernelbase", argv[1]); + if (!address) + return find_ctrl_routine_the_hard_way (); + } + + if (!address) + { + fprintf (stderr, "Could not get proc address\n"); + return 1; + } + + if (pid == 0) + { + printf ("%p\n", address); + fflush (stdout); + return 0; + } + HANDLE h = OpenProcess (PROCESS_CREATE_THREAD | + PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | + PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pid); + if (h == NULL) + { + fprintf (stderr, "OpenProcess failed: %ld\n", GetLastError ()); + return 1; + } + /* Inject the remote thread */ + if (inject_remote_thread_into_process (h, (LPTHREAD_START_ROUTINE)address, + exit_code, &thread_return) < 0) + { + fprintf (stderr, "Could not inject thread into process %lu\n", pid); + return 1; + } + + if (is_ctrl_routine && thread_return) + { + fprintf (stderr, + "Injected remote thread for pid %lu returned %lu\n", pid, + thread_return); + return 1; + } + + return 0; +} From c93ce39dfdf7252b283db008dac7bc45990cf44f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Mar 2015 09:56:28 +0000 Subject: [PATCH 055/102] Emulate GenerateConsoleCtrlEvent() upon Ctrl+C This patch is heavily inspired by the Git for Windows' strategy in handling Ctrl+C. When a process is terminated via TerminateProcess(), it has no chance to do anything in the way of cleaning up. This is particularly noticeable when a lengthy Git for Windows process tries to update Git's index file and leaves behind an index.lock file. Git's idea is to remove the stale index.lock file in that case, using the signal and atexit handlers available in Linux. But those signal handlers never run. Note: this is not an issue for MSYS2 processes because MSYS2 emulates Unix' signal system accurately, both for the process sending the kill signal and the process receiving it. Win32 processes do not have such a signal handler, though, instead MSYS2 shuts them down via `TerminateProcess()`. For a while, Git for Windows tried to use a gentler method, described in the Dr Dobb's article "A Safer Alternative to TerminateProcess()" by Andrew Tucker (July 1, 1999), http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547 Essentially, we injected a new thread into the running process that does nothing else than running the ExitProcess() function. However, this was still not in line with the way CMD handles Ctrl+C: it gives processes a chance to do something upon Ctrl+C by calling SetConsoleCtrlHandler(), and ExitProcess() simply never calls that handler. So for a while we tried to handle SIGINT/SIGTERM by attaching to the console of the command to interrupt, and generating the very same event as CMD does via GenerateConsoleCtrlEvent(). This method *still* was not correct, though, as it would interrupt *every* process attached to that Console, not just the process (and its children) that we wanted to signal. A symptom was that hitting Ctrl+C while `git log` was shown in the pager would interrupt *the pager*. The method we settled on is to emulate what GenerateConsoleCtrlEvent() does, but on a process by process basis: inject a remote thread and call the (private) function kernel32!CtrlRoutine. To obtain said function's address, we use the dbghelp API to generate a stack trace from a handler configured via SetConsoleCtrlHandler() and triggered via GenerateConsoleCtrlEvent(). To avoid killing each and all processes attached to the same Console as the MSYS2 runtime, we modify the cygwin-console-helper to optionally print the address of kernel32!CtrlRoutine to stdout, and then spawn it with a new Console. Note that this also opens the door to handling 32-bit process from a 64-bit MSYS2 runtime and vice versa, by letting the MSYS2 runtime look for the cygwin-console-helper.exe of the "other architecture" in a specific place (we choose /usr/libexec/, as it seems to be the convention for helper .exe files that are not intended for public consumption). The 32-bit helper implicitly links to libgcc_s_dw2.dll and libwinpthread-1.dll, so to avoid cluttering /usr/libexec/, we look for the helped of the "other" architecture in the corresponding mingw32/ or mingw64/ subdirectory. Among other bugs, this strategy to handle Ctrl+C fixes the MSYS2 side of the bug where interrupting `git clone https://...` would send the spawned-off `git remote-https` process into the background instead of interrupting it, i.e. the clone would continue and its progress would be reported mercilessly to the console window without the user being able to do anything about it (short of firing up the task manager and killing the appropriate task manually). Note that this special-handling is only necessary when *MSYS2* handles the Ctrl+C event, e.g. when interrupting a process started from within MinTTY or any other non-cmd-based terminal emulator. If the process was started from within `cmd.exe`'s terminal window, child processes are already killed appropriately upon Ctrl+C, by `cmd.exe` itself. Also, we can't trust the processes to end it's subprocesses upon receiving Ctrl+C. For example, `pip.exe` from `python-pip` doesn't kill the python it lauches (it tries to but fails), and I noticed that in cmd it kills python also correctly, which mean we should kill all the process using `exit_process_tree`. Co-authored-by: Naveen M K Signed-off-by: Johannes Schindelin --- winsup/cygwin/exceptions.cc | 24 +- winsup/cygwin/include/cygwin/exit_process.h | 364 ++++++++++++++++++++ 2 files changed, 384 insertions(+), 4 deletions(-) create mode 100644 winsup/cygwin/include/cygwin/exit_process.h diff --git a/winsup/cygwin/exceptions.cc b/winsup/cygwin/exceptions.cc index 3b7f23bf60..ca86b36438 100644 --- a/winsup/cygwin/exceptions.cc +++ b/winsup/cygwin/exceptions.cc @@ -29,6 +29,7 @@ details. */ #include "exception.h" #include "posix_timer.h" #include "gcc_seh.h" +#include "cygwin/exit_process.h" /* Define macros for CPU-agnostic register access. The _CX_foo macros are for access into CONTEXT, the _MC_foo ones for access into @@ -1671,10 +1672,25 @@ sigpacket::process () dosig: if (have_execed && (ch_spawn.iscygwin () || !is_stop_or_cont (si.si_signo))) { - sigproc_printf ("terminating captive process"); - if (::cygheap->ctty) - ::cygheap->ctty->cleanup_before_exit (); - TerminateProcess (ch_spawn, sigExeced = si.si_signo); + switch (si.si_signo) + { + case SIGUSR1: + case SIGUSR2: + case SIGCONT: + case SIGSTOP: + case SIGTSTP: + case SIGTTIN: + case SIGTTOU: + system_printf ("Suppressing signal %d to win32 process (pid %u)", + (int)si.si_signo, (unsigned int)GetProcessId(ch_spawn)); + goto done; + default: + sigproc_printf ("terminating captive process"); + if (::cygheap->ctty) + ::cygheap->ctty->cleanup_before_exit (); + rc = exit_process_tree (ch_spawn, 128 + (sigExeced = si.si_signo)); + goto done; + } } /* Dispatch to the appropriate function. */ sigproc_printf ("signal %d, signal handler %p", si.si_signo, handler); diff --git a/winsup/cygwin/include/cygwin/exit_process.h b/winsup/cygwin/include/cygwin/exit_process.h new file mode 100644 index 0000000000..0486a0c74a --- /dev/null +++ b/winsup/cygwin/include/cygwin/exit_process.h @@ -0,0 +1,364 @@ +#ifndef EXIT_PROCESS_H +#define EXIT_PROCESS_H + +/* + * This file contains functions to terminate a Win32 process, as gently as + * possible. + * + * If appropriate, we will attempt to emulate a console Ctrl event for the + * process. Otherwise we will fall back to terminating the process. + * + * As we do not want to export this function in the MSYS2 runtime, these + * functions are marked as file-local. + * + * The idea is to inject a thread into the given process that runs either + * kernel32!CtrlRoutine() (i.e. the work horse of GenerateConsoleCtrlEvent()) + * for SIGINT (Ctrl+C) and SIGQUIT (Ctrl+Break), or ExitProcess() for SIGTERM. + * This is handled through the console helpers. + * + * For SIGKILL, we run TerminateProcess() without injecting anything, and this + * is also the fall-back when the previous methods are unavailable. + * + * Note: as kernel32.dll is loaded before any process, the other process and + * this process will have ExitProcess() at the same address. The same holds + * true for kernel32!CtrlRoutine(), of course, but it is an internal API + * function, so we cannot look it up directly. Instead, we launch + * getprocaddr.exe to find out and inject the remote thread. + * + * This function expects the process handle to have the access rights for + * CreateRemoteThread(): PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, + * PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ. + * + * The idea for the injected remote thread comes from the Dr Dobb's article "A + * Safer Alternative to TerminateProcess()" by Andrew Tucker (July 1, 1999), + * http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547. + * + * The idea to use kernel32!CtrlRoutine for the other signals comes from + * SendSignal (https://github.com/AutoSQA/SendSignal/ and + * http://stanislavs.org/stopping-command-line-applications-programatically-with-ctrl-c-events-from-net/). + */ + +#include +#include + +#ifndef __INSIDE_CYGWIN__ +/* To help debugging via kill.exe */ +#define small_printf(...) fprintf (stderr, __VA_ARGS__) +#endif + +static BOOL get_wow (HANDLE process, BOOL &is_wow, USHORT &process_arch); +static int exit_process_tree (HANDLE main_process, int exit_code); + +static BOOL +kill_via_console_helper (HANDLE process, wchar_t *function_name, int exit_code, + DWORD pid) +{ + BOOL is_wow; + USHORT process_arch; + if (!get_wow (process, is_wow, process_arch)) + { + return FALSE; + } + + const char *name; + switch (process_arch) + { + case IMAGE_FILE_MACHINE_I386: + name = "/usr/libexec/getprocaddr32.exe"; + break; + case IMAGE_FILE_MACHINE_AMD64: + name = "/usr/libexec/getprocaddr64.exe"; + break; + /* TODO: provide exes for these */ + case IMAGE_FILE_MACHINE_ARMNT: + name = "/usr/libexec/getprocaddrarm32.exe"; + break; + case IMAGE_FILE_MACHINE_ARM64: + name = "/usr/libexec/getprocaddrarm64.exe"; + break; + default: + return FALSE; /* what?!? */ + } + wchar_t wbuf[PATH_MAX]; + + if (cygwin_conv_path (CCP_POSIX_TO_WIN_W, name, wbuf, PATH_MAX) + || GetFileAttributesW (wbuf) == INVALID_FILE_ATTRIBUTES) + return FALSE; + + STARTUPINFOW si = {}; + PROCESS_INFORMATION pi; + size_t len = wcslen (wbuf) + 1 /* space */ + wcslen (function_name) + + 1 /* space */ + 3 /* exit code */ + 1 /* space */ + + 10 /* process ID, i.e. DWORD */ + 1 /* NUL */; + WCHAR cmd[len + 1]; + WCHAR title[] = L"cygwin-console-helper"; + DWORD process_exit; + + swprintf (cmd, len + 1, L"%S %S %d %u", wbuf, function_name, exit_code, + pid); + + si.cb = sizeof (si); + si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES; + si.wShowWindow = SW_HIDE; + si.lpTitle = title; + si.hStdInput = si.hStdError = si.hStdOutput = INVALID_HANDLE_VALUE; + + /* Create a new hidden process. */ + if (!CreateProcessW (NULL, cmd, NULL, NULL, TRUE, + CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP, NULL, NULL, + &si, &pi)) + { + return FALSE; + } + else + { + /* Wait for the process to complete for 10 seconds */ + WaitForSingleObject (pi.hProcess, 10000); + } + + if (!GetExitCodeProcess (pi.hProcess, &process_exit)) + process_exit = -1; + + CloseHandle (pi.hThread); + CloseHandle (pi.hProcess); + + return process_exit == 0 ? TRUE : FALSE; +} + +static int current_is_wow = -1; +static int is_32_bit_os = -1; + +typedef BOOL (WINAPI * IsWow64Process2_t) (HANDLE, USHORT *, USHORT *); +static bool wow64process2initialized = false; +static IsWow64Process2_t pIsWow64Process2 /* = NULL */; + +typedef BOOL (WINAPI * GetProcessInformation_t) (HANDLE, + PROCESS_INFORMATION_CLASS, + LPVOID, DWORD); +static bool getprocessinfoinitialized = false; +static GetProcessInformation_t pGetProcessInformation /* = NULL */; + +static BOOL +get_wow (HANDLE process, BOOL &is_wow, USHORT &process_arch) +{ + USHORT native_arch = IMAGE_FILE_MACHINE_UNKNOWN; + if (!wow64process2initialized) + { + pIsWow64Process2 = (IsWow64Process2_t) + GetProcAddress (GetModuleHandle ("KERNEL32"), + "IsWow64Process2"); + MemoryBarrier (); + wow64process2initialized = true; + } + if (!pIsWow64Process2) + { + if (is_32_bit_os == -1) + { + SYSTEM_INFO info; + + GetNativeSystemInfo (&info); + if (info.wProcessorArchitecture == 0) + is_32_bit_os = 1; + else if (info.wProcessorArchitecture == 9) + is_32_bit_os = 0; + else + is_32_bit_os = -2; + } + + if (current_is_wow == -1 + && !IsWow64Process (GetCurrentProcess (), ¤t_is_wow)) + current_is_wow = -2; + + if (is_32_bit_os == -2 || current_is_wow == -2) + return FALSE; + + if (!IsWow64Process (process, &is_wow)) + return FALSE; + + process_arch = is_32_bit_os || is_wow ? IMAGE_FILE_MACHINE_I386 : + IMAGE_FILE_MACHINE_AMD64; + return TRUE; + } + + if (!pIsWow64Process2 (process, &process_arch, &native_arch)) + return FALSE; + + /* The value will be IMAGE_FILE_MACHINE_UNKNOWN if the target process + * is not a WOW64 process + */ + if (process_arch == IMAGE_FILE_MACHINE_UNKNOWN) + { + struct /* _PROCESS_MACHINE_INFORMATION */ + { + /* 0x0000 */ USHORT ProcessMachine; + /* 0x0002 */ USHORT Res0; + /* 0x0004 */ DWORD MachineAttributes; + } /* size: 0x0008 */ process_machine_info; + + is_wow = FALSE; + /* However, x86_64 on ARM64 claims not to be WOW64, so we have to + * dig harder... */ + if (!getprocessinfoinitialized) + { + pGetProcessInformation = (GetProcessInformation_t) + GetProcAddress (GetModuleHandle ("KERNEL32"), + "GetProcessInformation"); + MemoryBarrier (); + getprocessinfoinitialized = true; + } + /*#define ProcessMachineTypeInfo 9*/ + if (pGetProcessInformation && + pGetProcessInformation (process, (PROCESS_INFORMATION_CLASS)9, + &process_machine_info, sizeof (process_machine_info))) + process_arch = process_machine_info.ProcessMachine; + else + process_arch = native_arch; + } + else + { + is_wow = TRUE; + } + return TRUE; +} + +/** + * Terminates the process corresponding to the process ID + * + * This way of terminating the processes is not gentle: the process gets + * no chance of cleaning up after itself (closing file handles, removing + * .lock files, terminating spawned processes (if any), etc). + */ +static int +exit_process (HANDLE process, int exit_code) +{ + LPTHREAD_START_ROUTINE address = NULL; + DWORD pid = GetProcessId (process), code; + int signo = exit_code & 0x7f; + switch (signo) + { + case SIGINT: + case SIGQUIT: + /* We are not going to kill them but simply say that Ctrl+C + is pressed. If the processes want they can exit or else + just wait.*/ + if (kill_via_console_helper ( + process, L"CtrlRoutine", + signo == SIGINT ? CTRL_C_EVENT : CTRL_BREAK_EVENT, pid)) + return 0; + /* fall-through */ + case SIGTERM: + if (kill_via_console_helper (process, L"ExitProcess", exit_code, pid)) + return 0; + break; + default: + break; + } + + return int (TerminateProcess (process, exit_code)); +} + +#include +#include + +/** + * Terminates the process corresponding to the process ID and all of its + * directly and indirectly spawned subprocesses using the + * TerminateProcess() function. + */ +static int +exit_process_tree (HANDLE main_process, int exit_code) +{ + HANDLE snapshot = CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0); + PROCESSENTRY32 entry; + DWORD pids[16384]; + int max_len = sizeof (pids) / sizeof (*pids), i, len, ret = 0; + DWORD pid = GetProcessId (main_process); + int signo = exit_code & 0x7f; + + pids[0] = pid; + len = 1; + + /* + * Even if Process32First()/Process32Next() seem to traverse the + * processes in topological order (i.e. parent processes before + * child processes), there is nothing in the Win32 API documentation + * suggesting that this is guaranteed. + * + * Therefore, run through them at least twice and stop when no more + * process IDs were added to the list. + */ + for (;;) + { + memset (&entry, 0, sizeof (entry)); + entry.dwSize = sizeof (entry); + + if (!Process32First (snapshot, &entry)) + break; + + int orig_len = len; + do + { + /** + * Look for the parent process ID in the list of pids to kill, and if + * found, add it to the list. + */ + for (i = len - 1; i >= 0; i--) + { + if (pids[i] == entry.th32ProcessID) + break; + if (pids[i] != entry.th32ParentProcessID) + continue; + + /* We found a process to kill; is it an MSYS2 process? */ + pid_t cyg_pid = cygwin_winpid_to_pid (entry.th32ProcessID); + if (cyg_pid > -1) + { + if (cyg_pid == getpgid (cyg_pid)) + kill (cyg_pid, signo); + break; + } + pids[len++] = entry.th32ProcessID; + break; + } + } + while (len < max_len && Process32Next (snapshot, &entry)); + + if (orig_len == len || len >= max_len) + break; + } + + CloseHandle (snapshot); + + for (i = len - 1; i >= 0; i--) + { + HANDLE process; + + if (!i) + process = main_process; + else + { + process = OpenProcess ( + PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION + | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, + FALSE, pids[i]); + if (!process) + process = OpenProcess ( + PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE, + FALSE, pids[i]); + } + DWORD code; + + if (process + && (!GetExitCodeProcess (process, &code) || code == STILL_ACTIVE)) + { + if (!exit_process (process, exit_code)) + ret = -1; + } + if (process && process != main_process) + CloseHandle (process); + } + + return ret; +} + +#endif From 5df8ad8f697c4937c859fed3bb1369b93b5afea3 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Mar 2015 10:01:50 +0000 Subject: [PATCH 056/102] kill: kill Win32 processes more gently This change is the equivalent to the change to the Ctrl+C handling we just made. Co-authored-by: Naveen M K Signed-off-by: Johannes Schindelin --- winsup/utils/kill.cc | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/winsup/utils/kill.cc b/winsup/utils/kill.cc index bcabcd47c9..31ad57a137 100644 --- a/winsup/utils/kill.cc +++ b/winsup/utils/kill.cc @@ -17,6 +17,7 @@ details. */ #include #include #include +#include static char *prog_name; @@ -300,10 +301,20 @@ forcekill (pid_t pid, DWORD winpid, int sig, int wait) return; } if (!wait || WaitForSingleObject (h, 200) != WAIT_OBJECT_0) - if (sig && !TerminateProcess (h, sig << 8) - && WaitForSingleObject (h, 200) != WAIT_OBJECT_0) - fprintf (stderr, "%s: couldn't kill pid %u, %u\n", - prog_name, (unsigned int) dwpid, (unsigned int) GetLastError ()); + { + HANDLE cur = GetCurrentProcess (), h2; + /* duplicate handle with access rights required for exit_process_tree() */ + if (DuplicateHandle (cur, h, cur, &h2, PROCESS_CREATE_THREAD | + PROCESS_QUERY_INFORMATION | + PROCESS_VM_OPERATION | + PROCESS_VM_WRITE | PROCESS_VM_READ | + PROCESS_TERMINATE, FALSE, 0)) + { + CloseHandle(h); + h = h2; + } + exit_process_tree (h, 128 + sig); + } CloseHandle (h); } From 369346ae291771408e7db4ec378caf254092f382 Mon Sep 17 00:00:00 2001 From: Jeremy Drake Date: Thu, 22 Jul 2021 11:59:16 -0700 Subject: [PATCH 057/102] Cygwin: make option for native inner link handling. This code has been causing issues with SUBST and mapped network drives, so add an option (defaulted to on) which can be used to disable it where needed. MSYS=nonativeinnerlinks --- winsup/cygwin/environ.cc | 1 + winsup/cygwin/globals.cc | 1 + winsup/cygwin/path.cc | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/winsup/cygwin/environ.cc b/winsup/cygwin/environ.cc index b9600ef9a8..06b1111f51 100644 --- a/winsup/cygwin/environ.cc +++ b/winsup/cygwin/environ.cc @@ -123,6 +123,7 @@ static struct parse_thing {"error_start", {func: error_start_init}, isfunc, NULL, {{0}, {0}}}, {"export", {&export_settings}, setbool, NULL, {{false}, {true}}}, {"glob", {func: glob_init}, isfunc, NULL, {{0}, {s: "normal"}}}, + {"nativeinnerlinks", {&nativeinnerlinks}, setbool, NULL, {{false}, {true}}}, {"pipe_byte", {&pipe_byte}, setbool, NULL, {{false}, {true}}}, {"proc_retry", {func: set_proc_retry}, isfunc, NULL, {{0}, {5}}}, {"reset_com", {&reset_com}, setbool, NULL, {{false}, {true}}}, diff --git a/winsup/cygwin/globals.cc b/winsup/cygwin/globals.cc index 79f9476330..30a2da1205 100644 --- a/winsup/cygwin/globals.cc +++ b/winsup/cygwin/globals.cc @@ -74,6 +74,7 @@ bool wincmdln = true; winsym_t allow_winsymlinks = WSYM_deepcopy; bool disable_pcon; bool winjitdebug = false; +bool nativeinnerlinks = true; /* Taken from BSD libc: This variable is zero until a process has created a pthread. It is used diff --git a/winsup/cygwin/path.cc b/winsup/cygwin/path.cc index aed115792c..9618676b0b 100644 --- a/winsup/cygwin/path.cc +++ b/winsup/cygwin/path.cc @@ -3848,8 +3848,9 @@ symlink_info::check (char *path, const suffix_info *suffixes, fs_info &fs, differ, return the final path as symlink content and set symlen to a negative value. This forces path_conv::check to restart symlink evaluation with the new path. */ - if ((pc_flags () & (PC_SYM_FOLLOW | PC_SYM_NOFOLLOW_REP)) - == PC_SYM_FOLLOW) + if (nativeinnerlinks + && (pc_flags () & (PC_SYM_FOLLOW | PC_SYM_NOFOLLOW_REP)) + == PC_SYM_FOLLOW) { PWCHAR fpbuf = tp.w_get (); DWORD ret; From aa1cb2c3f75a343007f949c7e823aad303f3313a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 8 Nov 2021 14:20:07 +0100 Subject: [PATCH 058/102] docs: skip building texinfo and PDF files The MSYS2 packages lack the infrastructure to build those. Signed-off-by: Johannes Schindelin --- winsup/configure.ac | 7 +++---- winsup/doc/Makefile.am | 9 +++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/winsup/configure.ac b/winsup/configure.ac index b9e3977fcf..b88f3ade29 100644 --- a/winsup/configure.ac +++ b/winsup/configure.ac @@ -84,11 +84,10 @@ AM_CONDITIONAL(BUILD_DOC, [test $enable_doc != "no"]) AC_CHECK_PROGS([DOCBOOK2XTEXI], [docbook2x-texi db2x_docbook2texi]) if test -z "$DOCBOOK2XTEXI" ; then if test "x$enable_doc" != "xno"; then - AC_MSG_ERROR([docbook2texi is required to build documentation]) - else - unset DOCBOOK2XTEXI - AM_MISSING_PROG([DOCBOOK2XTEXI], [docbook2texi]) + AC_MSG_WARN([docbook2texi is required to build documentation]) fi + unset DOCBOOK2XTEXI + AM_MISSING_PROG([DOCBOOK2XTEXI], [docbook2texi]) fi AC_CHECK_PROGS([XMLTO], [xmlto]) diff --git a/winsup/doc/Makefile.am b/winsup/doc/Makefile.am index e3ee326123..e6fec84922 100644 --- a/winsup/doc/Makefile.am +++ b/winsup/doc/Makefile.am @@ -10,9 +10,7 @@ man1_MANS = man3_MANS = man5_MANS = -doc_DATA = \ - cygwin-ug-net/cygwin-ug-net.pdf \ - cygwin-api/cygwin-api.pdf +doc_DATA = htmldir = $(datarootdir)/doc @@ -35,8 +33,7 @@ all-local: Makefile.dep \ cygwin-ug-net/cygwin-ug-net.html \ faq/faq.html faq/faq.body \ cygwin-ug-net/cygwin-ug-net-nochunks.html.gz \ - api2man.stamp intro2man.stamp utils2man.stamp \ - cygwin-api.info cygwin-ug-net.info + api2man.stamp intro2man.stamp utils2man.stamp clean-local: rm -f Makefile.dep @@ -76,7 +73,7 @@ install-etc: @$(MKDIR_P) $(DESTDIR)$(sysconfdir)/preremove $(INSTALL_SCRIPT) $(srcdir)/etc.preremove.cygwin-doc.sh $(DESTDIR)$(sysconfdir)/preremove/cygwin-doc.sh -install-data-hook: install-extra-man install-html-local install-info-local install-etc +install-data-hook: install-extra-man install-html-local install-etc uninstall-extra-man: for i in *.1 ; do \ From 0f9b6dec120e664fe67e666fec9aabbc959b8e5a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 8 Nov 2021 16:22:57 +0100 Subject: [PATCH 059/102] install-libs: depend on the "toollibs" Before symlinking libg.a, we need the symlink source `libmsys-2.0.a`: in MSYS2, we copy by default (if we were creating Unix-style symlinks, the target would not have to exist before symlinking, but when copying we do need the source _right away_). Signed-off-by: Johannes Schindelin --- winsup/cygwin/Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winsup/cygwin/Makefile.am b/winsup/cygwin/Makefile.am index 54ae637450..9c09fc2170 100644 --- a/winsup/cygwin/Makefile.am +++ b/winsup/cygwin/Makefile.am @@ -707,7 +707,7 @@ man_MANS = regex/regex.3 regex/regex.7 install-exec-hook: install-libs install-data-local: install-headers install-ldif -install-libs: +install-libs: install-toollibDATA @$(MKDIR_P) $(DESTDIR)$(bindir) $(INSTALL_PROGRAM) $(NEW_DLL_NAME) $(DESTDIR)$(bindir)/$(DLL_NAME) @$(MKDIR_P) $(DESTDIR)$(toollibdir) From 3244d7c088c747b01632ac4c4cc41fea6a8c75d6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 23 Nov 2015 20:03:11 +0100 Subject: [PATCH 060/102] POSIX-ify the SHELL variable When calling a non-MSys2 binary, all of the environment is converted from POSIX to Win32, including the SHELL environment variable. In Git for Windows, for example, `SHELL=/usr/bin/bash` is converted to `SHELL=C:\Program Files\Git\usr\bin\bash.exe` when calling the `git.exe` binary. This is appropriate because non-MSys2 binaries would not handle POSIX paths correctly. Under certain circumstances, however, `git.exe` calls an *MSys2* binary in turn, such as `git config --edit` calling `vim.exe` unless Git is configured to use another editor specifically. Now, when this "improved vi" calls shell commands, it uses that $SHELL variable *without quoting*, resulting in a nasty error: C:\Program: No such file or directory Many other programs behave in the same manner, assuming that $SHELL does not contain spaces and hence needs no quoting, unfortunately including some of Git's own scripts. Therefore let's make sure that $SHELL gets "posified" again when entering MSys2 programs. Earlier attempts by Git for Windows contributors claimed that adding `SHELL` to the `conv_envvars` array does not have the intended effect. These reports just missed that the `conv_start_chars` array (which makes the code more performant) needs to be adjusted, too. Note that we set the `immediate` flag to `true` so that the environment variable is set immediately by the MSys2 runtime, i.e. not only spawned processes will see the POSIX-ified `SHELL` variable, but the MSys2 runtime *itself*, too. This fixes https://github.com/git-for-windows/git/issues/542, https://github.com/git-for-windows/git/issues/498, and https://github.com/git-for-windows/git/issues/468. Signed-off-by: Johannes Schindelin --- winsup/cygwin/environ.cc | 8 +++++++- winsup/cygwin/local_includes/environ.h | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/winsup/cygwin/environ.cc b/winsup/cygwin/environ.cc index 06b1111f51..e21c8fddb9 100644 --- a/winsup/cygwin/environ.cc +++ b/winsup/cygwin/environ.cc @@ -323,6 +323,7 @@ static win_env conv_envvars[] = {NL ("HOME="), NULL, NULL, env_path_to_posix, env_path_to_win32, false}, {NL ("LD_LIBRARY_PATH="), NULL, NULL, env_plist_to_posix, env_plist_to_win32, true}, + {NL ("SHELL="), NULL, NULL, env_path_to_posix, env_path_to_win32, true, true}, {NL ("TMPDIR="), NULL, NULL, env_path_to_posix, env_path_to_win32, false}, {NL ("TMP="), NULL, NULL, env_path_to_posix, env_path_to_win32, false}, {NL ("TEMP="), NULL, NULL, env_path_to_posix, env_path_to_win32, false}, @@ -351,7 +352,7 @@ static const unsigned char conv_start_chars[256] = WC, 0, 0, 0, WC, 0, 0, 0, /* 80 */ /* P Q R S T U V W */ - WC, 0, 0, 0, WC, 0, 0, 0, + WC, 0, 0, WC, WC, 0, 0, 0, /* 88 */ /* x Y Z */ 0, 0, 0, 0, 0, 0, 0, 0, @@ -380,6 +381,7 @@ win_env::operator = (struct win_env& x) toposix = x.toposix; towin32 = x.towin32; immediate = false; + skip_if_empty = x.skip_if_empty; return *this; } @@ -401,6 +403,8 @@ win_env::add_cache (const char *in_posix, const char *in_native) native = (char *) realloc (native, namelen + 1 + strlen (in_native)); stpcpy (stpcpy (native, name), in_native); } + else if (skip_if_empty && !*in_posix) + native = (char *) calloc(1, 1); else { tmp_pathbuf tp; @@ -466,6 +470,8 @@ posify_maybe (char **here, const char *value, char *outenv) return; int len = strcspn (src, "=") + 1; + if (conv->skip_if_empty && !src[len]) + return; /* Turn all the items from c:; into their mounted equivalents - if there is one. */ diff --git a/winsup/cygwin/local_includes/environ.h b/winsup/cygwin/local_includes/environ.h index 0dd45359cc..fd6ca466e8 100644 --- a/winsup/cygwin/local_includes/environ.h +++ b/winsup/cygwin/local_includes/environ.h @@ -21,7 +21,7 @@ struct win_env char *native; ssize_t (*toposix) (const void *, void *, size_t); ssize_t (*towin32) (const void *, void *, size_t); - bool immediate; + bool immediate, skip_if_empty; void add_cache (const char *in_posix, const char *in_native = NULL); const char * get_native () const {return native ? native + namelen : NULL;} const char * get_posix () const {return posix ? posix : NULL;} From d71bdf6d35981079c6ad403ad04bb4980ac0e773 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 21 Mar 2017 13:18:38 +0100 Subject: [PATCH 061/102] Handle ORIGINAL_PATH just like PATH MSYS2 recently introduced that hack where the ORIGINAL_PATH variable is set to the original PATH value in /etc/profile, unless previously set. In Git for Windows' default mode, that ORIGINAL_PATH value is the used to define the PATH variable explicitly. So far so good. The problem: when calling from inside an MSYS2 process (such as Bash) a MINGW executable (such as git.exe) that then calls another MSYS2 executable (such as bash.exe), that latter call will try to re-convert ORIGINAL_PATH after the previous call converted ORIGINAL_PATH from POSIX to Windows paths. And this conversion may very well fail, e.g. when the path list contains mixed semicolons and colons. So let's just *force* the MSYS2 runtime to handle ORIGINAL_PATH in the same way as the PATH variable (which conversion works, as we know). Signed-off-by: Johannes Schindelin --- winsup/cygwin/environ.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/winsup/cygwin/environ.cc b/winsup/cygwin/environ.cc index e21c8fddb9..031db039ea 100644 --- a/winsup/cygwin/environ.cc +++ b/winsup/cygwin/environ.cc @@ -323,6 +323,7 @@ static win_env conv_envvars[] = {NL ("HOME="), NULL, NULL, env_path_to_posix, env_path_to_win32, false}, {NL ("LD_LIBRARY_PATH="), NULL, NULL, env_plist_to_posix, env_plist_to_win32, true}, + {NL ("ORIGINAL_PATH="), NULL, NULL, env_PATH_to_posix, env_plist_to_win32, true}, {NL ("SHELL="), NULL, NULL, env_path_to_posix, env_path_to_win32, true, true}, {NL ("TMPDIR="), NULL, NULL, env_path_to_posix, env_path_to_win32, false}, {NL ("TMP="), NULL, NULL, env_path_to_posix, env_path_to_win32, false}, @@ -349,7 +350,7 @@ static const unsigned char conv_start_chars[256] = 0, 0, 0, 0, 0, 0, 0, 0, /* 72 */ /* H I J K L M N O */ - WC, 0, 0, 0, WC, 0, 0, 0, + WC, 0, 0, 0, WC, 0, 0, WC, /* 80 */ /* P Q R S T U V W */ WC, 0, 0, WC, WC, 0, 0, 0, From fc06dfda92f00c10a106ede8ba58853e567edb82 Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Sun, 3 Jul 2022 22:39:32 +0200 Subject: [PATCH 062/102] uname: allow setting the system name to CYGWIN We are currently trying to move our cygwin build environment closer to cygwin and some autotools/bash based build systems call "uname -s" to figure out the OS and in many cases only handle the cygwin case, so we have to patch them. With this instead of patching we can set MSYSTEM=CYGWIN and change uname output that way. The next step would be to always output CYGWIN in an msys env by default, but for now this allows us to get rid of all the patches without affecting users. --- winsup/cygwin/uname.cc | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/winsup/cygwin/uname.cc b/winsup/cygwin/uname.cc index ed4c9c59a1..cca66be45a 100644 --- a/winsup/cygwin/uname.cc +++ b/winsup/cygwin/uname.cc @@ -24,6 +24,24 @@ extern "C" int getdomainname (char *__name, size_t __len); #define ATTRIBUTE_NONSTRING #endif +static const char* +get_sysname() +{ +#ifdef __MSYS__ + char* msystem = getenv("MSYSTEM"); + if (!msystem || strcmp(msystem, "MSYS") == 0) + return "MSYS"; + else if (strcmp(msystem, "CYGWIN") == 0) + return "CYGWIN"; + else if (strstr(msystem, "32") != NULL) + return "MINGW32"; + else + return "MINGW64"; +#else + return "CYGWIN"; +#endif +} + /* uname: POSIX 4.4.1.1 */ /* New entrypoint for applications since API 335 */ @@ -37,12 +55,9 @@ uname_x (struct utsname *name) memset (name, 0, sizeof (*name)); /* sysname */ - char* msystem = getenv("MSYSTEM"); - const char* msystem_sysname = "MSYS"; - if (msystem != NULL && *msystem && strcmp(msystem, "MSYS") != 0) - msystem_sysname = (strstr(msystem, "32") != NULL) ? "MINGW32" : "MINGW64";; + const char* sysname = get_sysname(); n = __small_sprintf (name->sysname, "%s_%s-%u", - msystem_sysname, + sysname, wincap.osname (), wincap.build_number ()); if (wincap.host_machine () != wincap.cygwin_machine ()) { @@ -123,15 +138,8 @@ uname (struct utsname *in_name) __try { memset (name, 0, sizeof (*name)); -#ifdef __MSYS__ - char* msystem = getenv("MSYSTEM"); - const char* msystem_sysname = "MSYS"; - if (msystem != NULL && *msystem && strcmp(msystem, "MSYS") != 0) - msystem_sysname = (strstr(msystem, "32") != NULL) ? "MINGW32" : "MINGW64"; - __small_sprintf (name->sysname, "%s_%s", msystem_sysname, wincap.osname ()); -#else - __small_sprintf (name->sysname, "CYGWIN_%s", wincap.osname ()); -#endif + const char* sysname = get_sysname(); + __small_sprintf (name->sysname, "%s_%s", sysname, wincap.osname ()); /* Computer name */ cygwin_gethostname (name->nodename, sizeof (name->nodename) - 1); From 2bda763e12ce736ffc65a07039d4c1aa138adb0c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 18 Feb 2015 12:32:17 +0000 Subject: [PATCH 063/102] Pass environment variables with empty values There is a difference between an empty value and an unset environment variable. We should not confuse both; If the user wants to unset an environment variable, they can certainly do so (unsetenv(3), or in the shell: 'unset ABC'). This fixes Git's t3301-notes.sh, which overrides environment variables with empty values. Signed-off-by: Johannes Schindelin --- winsup/cygwin/environ.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winsup/cygwin/environ.cc b/winsup/cygwin/environ.cc index 031db039ea..6b385cd03d 100644 --- a/winsup/cygwin/environ.cc +++ b/winsup/cygwin/environ.cc @@ -1326,11 +1326,11 @@ build_env (const char * const *envp, PWCHAR &envblock, int &envc, Note that this doesn't stop invalid strings without '=' in it etc., but we're opting for speed here for now. Adding complete checking would be pretty expensive. */ - if (len == 1 || !*rest) + if (len == 1) continue; /* See if this entry requires posix->win32 conversion. */ - conv = getwinenv (*srcp, rest, &temp); + conv = !*rest ? NULL : getwinenv (*srcp, rest, &temp); if (conv) { p = conv->native; /* Use win32 path */ @@ -1344,7 +1344,7 @@ build_env (const char * const *envp, PWCHAR &envblock, int &envc, } } #ifdef __MSYS__ - else if (!keep_posix) { + else if (!keep_posix && *rest) { char *win_arg = arg_heuristic_with_exclusions (*srcp, msys2_env_conv_excl_env, msys2_env_conv_excl_count); debug_printf("WIN32_PATH is %s", win_arg); From 6588ea92d608e5bba44f6083bcdbc56178469dbb Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 6 Sep 2022 10:40:58 +0200 Subject: [PATCH 064/102] Optionally disallow empty environment values again We just disabled the code that skips environment variables whose values are empty. However, this code was introduced a long time ago into Cygwin in d6b1ac7faa (* environ.cc (build_env): Don't put an empty environment variable into the environment. Optimize use of "len". * errno.cc (ERROR_MORE_DATA): Translate to EMSGSIZE rather than EAGAIN., 2006-09-07), seemingly without any complaints. Meaning: There might very well be use cases out there where it makes sense to skip empty-valued environment variables. Therefore, it seems like a good idea to have a "knob" to turn it back on. With this commit, we introduce such a knob: by setting `noemptyenvvalues` the `MSYS` variable (or appending it if that variable is already set), users can tell the MSYS2 runtime to behave just like in the olden times. Signed-off-by: Johannes Schindelin --- winsup/cygwin/environ.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/winsup/cygwin/environ.cc b/winsup/cygwin/environ.cc index 6b385cd03d..12b4d57563 100644 --- a/winsup/cygwin/environ.cc +++ b/winsup/cygwin/environ.cc @@ -36,6 +36,7 @@ static char **lastenviron; /* Parse CYGWIN options */ static NO_COPY bool export_settings = false; +static bool emptyenvvalues = true; enum settings { @@ -119,6 +120,7 @@ static struct parse_thing } known[] NO_COPY = { {"disable_pcon", {&disable_pcon}, setbool, NULL, {{false}, {true}}}, + {"emptyenvvalues", {&emptyenvvalues}, setbool, NULL, {{false}, {true}}}, {"enable_pcon", {&disable_pcon}, setnegbool, NULL, {{true}, {false}}}, {"error_start", {func: error_start_init}, isfunc, NULL, {{0}, {0}}}, {"export", {&export_settings}, setbool, NULL, {{false}, {true}}}, @@ -1326,7 +1328,7 @@ build_env (const char * const *envp, PWCHAR &envblock, int &envc, Note that this doesn't stop invalid strings without '=' in it etc., but we're opting for speed here for now. Adding complete checking would be pretty expensive. */ - if (len == 1) + if (len == 1 || (!emptyenvvalues && !*rest)) continue; /* See if this entry requires posix->win32 conversion. */ From 989297df2b71a70a18aa2e78b9c0574b164a237a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 6 Sep 2022 12:18:18 +0200 Subject: [PATCH 065/102] build_env(): respect the `MSYS` environment variable With this commit, you can call MSYS=noemptyenvvalues my-command and it does what is expected: to pass no empty-valued environment variables to `my-command`. Signed-off-by: Johannes Schindelin --- winsup/cygwin/environ.cc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/winsup/cygwin/environ.cc b/winsup/cygwin/environ.cc index 12b4d57563..4e049211e9 100644 --- a/winsup/cygwin/environ.cc +++ b/winsup/cygwin/environ.cc @@ -1204,7 +1204,11 @@ build_env (const char * const *envp, PWCHAR &envblock, int &envc, { bool calc_tl = !no_envblock; #ifdef __MSYS__ - if (!keep_posix) + if (ascii_strncasematch(*srcp, "MSYS=", 5)) + { + parse_options (*srcp + 5); + } + else if (!keep_posix) { /* Don't pass timezone environment to non-msys applications */ if (ascii_strncasematch(*srcp, "TZ=", 3)) From f53fdff9289550fc4d09d44430c0f83efc04f4a5 Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Sat, 17 Dec 2022 20:14:49 +0100 Subject: [PATCH 066/102] Revert "Cygwin: Enable dynamicbase on the Cygwin DLL by default" This reverts commit 943433b00cacdde0cb9507d0178770a2fb67bd71. This seems to fix fork errors under Docker, see https://cygwin.com/pipermail/cygwin/2022-December/252711.html --- winsup/cygwin/Makefile.am | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/winsup/cygwin/Makefile.am b/winsup/cygwin/Makefile.am index 9c09fc2170..140e01a12b 100644 --- a/winsup/cygwin/Makefile.am +++ b/winsup/cygwin/Makefile.am @@ -605,8 +605,7 @@ $(NEW_DLL_NAME): $(LDSCRIPT) libdll.a $(VERSION_OFILES) $(LIBSERVER)\ $(newlib_build)/libm.a $(newlib_build)/libc.a $(AM_V_CXXLD)$(CXX) $(CXXFLAGS) \ -mno-use-libstdc-wrappers \ - -Wl,--gc-sections -nostdlib -Wl,-T$(LDSCRIPT) \ - -Wl,--dynamicbase -static \ + -Wl,--gc-sections -nostdlib -Wl,-T$(LDSCRIPT) -static \ $${SOURCE_DATE_EPOCH:+-Wl,--no-insert-timestamp} \ -Wl,--heap=0 -Wl,--out-implib,msysdll.a -shared -o $@ \ -e @DLL_ENTRY@ $(DEF_FILE) \ From a921c18c69cd62b0f8df8c98f5d1e0af012ca527 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 30 Jan 2023 23:22:22 +0100 Subject: [PATCH 067/102] Avoid sharing cygheaps across Cygwin versions It frequently leads to problems when trying, say, to call from MSYS2's Bash into Cygwin's or Git for Windows', merely because sharing that data is pretty finicky. For example, using the MSYS2' Bash using the MSYS2 runtime version that is current at time of writing, trying to call Cygwin's programs fails in manners like this: $ /c/cygwin64/bin/uname -r 0 [main] uname (9540) child_copy: cygheap read copy failed, 0x800000000..0x800010BE0, done 0, windows pid 9540, Win32 error 6 680 [main] uname 880 C:\cygwin64\bin\uname.exe: *** fatal error - couldn't create signal pipe, Win32 error 5 with the rather misleading exit code 127 (a code which is reserved to indicate that a command was not found). Let's just treat the MSYS2 runtime and the Cygwin runtime as completely incompatible with one another, by virtue of using a different magic constant than merely `CHILD_INFO_MAGIC`. By using the msys2-runtime commit to modify that magic constant, we can even spawn programs using a different MSYS2 runtime (such as Git for Windows') because the commit serves as the tell-tale whether two MSYS2 runtime versions are compatible with each other. To support building in the MSYS2-packages repository (where we do not check out the `msys2-runtime` but instead check out Cygwin and apply patches on top), let's accept a hard-coded commit hash as `./configure` option. One consequence is that spawned MSYS processes using a different MSYS2 runtime will not be visible as such to the parent process, i.e. they cannot share any resources such as pseudo terminals. But that's okay, they are simply treated as if they were regular Win32 programs. Note: We have to use a very rare form of encoding the brackets in the `expr` calls: quadrigraphs (for a thorough explanation, see https://www.gnu.org/savannah-checkouts/gnu/autoconf/manual/autoconf-2.70/html_node/Quadrigraphs.html#Quadrigraphs). This is necessary because it is apparently impossible to encode brackets in `configure.ac` files otherwise. Signed-off-by: Johannes Schindelin --- winsup/configure.ac | 28 ++++++++++++++++++++++++++++ winsup/cygwin/Makefile.am | 3 +++ winsup/cygwin/dcrt0.cc | 2 +- winsup/cygwin/sigproc.cc | 2 +- 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/winsup/configure.ac b/winsup/configure.ac index b88f3ade29..3aa2b16bf8 100644 --- a/winsup/configure.ac +++ b/winsup/configure.ac @@ -57,6 +57,34 @@ AC_CHECK_TOOL(RANLIB, ranlib, ranlib) AC_CHECK_TOOL(STRIP, strip, strip) AC_CHECK_TOOL(WINDRES, windres, windres) +# Record msys2-runtime commit +AC_ARG_WITH([msys2-runtime-commit], + [AS_HELP_STRING([--with-msys2-runtime-commit=COMMIT], + [indicate the msys2-runtime commit corresponding to this build])], + [MSYS2_RUNTIME_COMMIT=$withval], [MSYS2_RUNTIME_COMMIT=yes]) +case "$MSYS2_RUNTIME_COMMIT" in +no) + MSYS2_RUNTIME_COMMIT= + MSYS2_RUNTIME_COMMIT_HEX=0 + ;; +yes|auto) + if MSYS2_RUNTIME_COMMIT="$(git --git-dir="$srcdir/../.git" rev-parse HEAD)" + then + MSYS2_RUNTIME_COMMIT_HEX="0x$(expr "$MSYS2_RUNTIME_COMMIT" : '\(.\{,8\}\)')ull" + else + AC_MSG_WARN([Could not determine msys2-runtime commit]) + MSYS2_RUNTIME_COMMIT= + MSYS2_RUNTIME_COMMIT_HEX=0 + fi + ;; +*) + expr "$MSYS2_RUNTIME_COMMIT" : '@<:@0-9a-f@:>@\{6,64\}$' || + AC_MSG_ERROR([Invalid commit name: "$MSYS2_RUNTIME_COMMIT"]) + MSYS2_RUNTIME_COMMIT_HEX="0x$(expr "$MSYS2_RUNTIME_COMMIT" : '\(.\{,8\}\)')ull" + ;; +esac +AC_SUBST(MSYS2_RUNTIME_COMMIT_HEX) + AC_ARG_ENABLE(debugging, [AS_HELP_STRING([--enable-debugging],[Build a cygwin DLL which has more consistency checking for debugging])], [case "${enableval}" in diff --git a/winsup/cygwin/Makefile.am b/winsup/cygwin/Makefile.am index 140e01a12b..58ef25dfe5 100644 --- a/winsup/cygwin/Makefile.am +++ b/winsup/cygwin/Makefile.am @@ -17,6 +17,9 @@ if TARGET_X86_64 COMMON_CFLAGS+=-mcmodel=small endif +VERSION_CFLAGS = -DMSYS2_RUNTIME_COMMIT_HEX="@MSYS2_RUNTIME_COMMIT_HEX@" +COMMON_CFLAGS += $(VERSION_CFLAGS) + AM_CFLAGS=$(cflags_common) $(COMMON_CFLAGS) AM_CXXFLAGS=$(cxxflags_common) $(COMMON_CFLAGS) -fno-threadsafe-statics diff --git a/winsup/cygwin/dcrt0.cc b/winsup/cygwin/dcrt0.cc index 4d622cdc28..33cad1e3fe 100644 --- a/winsup/cygwin/dcrt0.cc +++ b/winsup/cygwin/dcrt0.cc @@ -531,7 +531,7 @@ get_cygwin_startup_info () child_info *res = (child_info *) si.lpReserved2; if (si.cbReserved2 < EXEC_MAGIC_SIZE || !res - || res->intro != PROC_MAGIC_GENERIC || res->magic != CHILD_INFO_MAGIC) + || res->intro != PROC_MAGIC_GENERIC || res->magic != (CHILD_INFO_MAGIC ^ MSYS2_RUNTIME_COMMIT_HEX)) { strace.activate (false); res = NULL; diff --git a/winsup/cygwin/sigproc.cc b/winsup/cygwin/sigproc.cc index 4ff05967b0..dda1a71da9 100644 --- a/winsup/cygwin/sigproc.cc +++ b/winsup/cygwin/sigproc.cc @@ -897,7 +897,7 @@ int child_info::retry_count = 0; child_info::child_info (unsigned in_cb, child_info_types chtype, bool need_subproc_ready): msv_count (0), cb (in_cb), intro (PROC_MAGIC_GENERIC), - magic (CHILD_INFO_MAGIC), type (chtype), cygheap (::cygheap), + magic (CHILD_INFO_MAGIC ^ MSYS2_RUNTIME_COMMIT_HEX), type (chtype), cygheap (::cygheap), cygheap_max (::cygheap_max), flag (0), retry (child_info::retry_count), rd_proc_pipe (NULL), wr_proc_pipe (NULL), subproc_ready (NULL), sigmask (_my_tls.sigmask) From 98f9ae06598888a6df4957bac8892d71dc99bee1 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 21 Feb 2023 16:36:36 +0100 Subject: [PATCH 068/102] uname: report msys2-runtime commit hash, too Having just Cygwin's version in the output of `uname` is not helpful, as both MSYS2 as well as Git for Windows release intermediate versions of the MSYS2 runtime much more often than Cygwin runtime versions are released. Signed-off-by: Johannes Schindelin --- winsup/configure.ac | 10 ++++++++-- winsup/cygwin/Makefile.am | 6 ++++-- winsup/cygwin/scripts/mkvers.sh | 8 ++++++++ winsup/cygwin/uname.cc | 16 +++++++++------- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/winsup/configure.ac b/winsup/configure.ac index 3aa2b16bf8..4dd5ccb9f9 100644 --- a/winsup/configure.ac +++ b/winsup/configure.ac @@ -65,24 +65,30 @@ AC_ARG_WITH([msys2-runtime-commit], case "$MSYS2_RUNTIME_COMMIT" in no) MSYS2_RUNTIME_COMMIT= + MSYS2_RUNTIME_COMMIT_SHORT= MSYS2_RUNTIME_COMMIT_HEX=0 ;; yes|auto) if MSYS2_RUNTIME_COMMIT="$(git --git-dir="$srcdir/../.git" rev-parse HEAD)" then - MSYS2_RUNTIME_COMMIT_HEX="0x$(expr "$MSYS2_RUNTIME_COMMIT" : '\(.\{,8\}\)')ull" + MSYS2_RUNTIME_COMMIT_SHORT="$(expr "$MSYS2_RUNTIME_COMMIT" : '\(.\{,8\}\)')" + MSYS2_RUNTIME_COMMIT_HEX="0x${MSYS2_RUNTIME_COMMIT_SHORT}ul" else AC_MSG_WARN([Could not determine msys2-runtime commit]) MSYS2_RUNTIME_COMMIT= + MSYS2_RUNTIME_COMMIT_SHORT= MSYS2_RUNTIME_COMMIT_HEX=0 fi ;; *) expr "$MSYS2_RUNTIME_COMMIT" : '@<:@0-9a-f@:>@\{6,64\}$' || AC_MSG_ERROR([Invalid commit name: "$MSYS2_RUNTIME_COMMIT"]) - MSYS2_RUNTIME_COMMIT_HEX="0x$(expr "$MSYS2_RUNTIME_COMMIT" : '\(.\{,8\}\)')ull" + MSYS2_RUNTIME_COMMIT_SHORT="$(expr "$MSYS2_RUNTIME_COMMIT" : '\(.\{,8\}\)')" + MSYS2_RUNTIME_COMMIT_HEX="0x${MSYS2_RUNTIME_COMMIT_SHORT}ul" ;; esac +AC_SUBST(MSYS2_RUNTIME_COMMIT) +AC_SUBST(MSYS2_RUNTIME_COMMIT_SHORT) AC_SUBST(MSYS2_RUNTIME_COMMIT_HEX) AC_ARG_ENABLE(debugging, diff --git a/winsup/cygwin/Makefile.am b/winsup/cygwin/Makefile.am index 58ef25dfe5..41190bdb7a 100644 --- a/winsup/cygwin/Makefile.am +++ b/winsup/cygwin/Makefile.am @@ -17,7 +17,9 @@ if TARGET_X86_64 COMMON_CFLAGS+=-mcmodel=small endif -VERSION_CFLAGS = -DMSYS2_RUNTIME_COMMIT_HEX="@MSYS2_RUNTIME_COMMIT_HEX@" +VERSION_CFLAGS = -DMSYS2_RUNTIME_COMMIT="\"@MSYS2_RUNTIME_COMMIT@\"" +VERSION_CFLAGS += -DMSYS2_RUNTIME_COMMIT_SHORT="\"@MSYS2_RUNTIME_COMMIT_SHORT@\"" +VERSION_CFLAGS += -DMSYS2_RUNTIME_COMMIT_HEX="@MSYS2_RUNTIME_COMMIT_HEX@" COMMON_CFLAGS += $(VERSION_CFLAGS) AM_CFLAGS=$(cflags_common) $(COMMON_CFLAGS) @@ -454,7 +456,7 @@ uname_version.c: .FORCE version.cc: scripts/mkvers.sh include/cygwin/version.h winver.rc $(src_files) @echo "Making version.cc and winver.o";\ export CC="$(CC)";\ - /bin/sh $(word 1,$^) $(word 2,$^) $(word 3,$^) $(WINDRES) $(CFLAGS) + /bin/sh $(word 1,$^) $(word 2,$^) $(word 3,$^) $(WINDRES) $(CFLAGS) $(VERSION_CFLAGS) winver.o: version.cc diff --git a/winsup/cygwin/scripts/mkvers.sh b/winsup/cygwin/scripts/mkvers.sh index a3d45c5db0..34d8d6dce1 100755 --- a/winsup/cygwin/scripts/mkvers.sh +++ b/winsup/cygwin/scripts/mkvers.sh @@ -16,6 +16,7 @@ incfile="$1"; shift rcfile="$1"; shift windres="$1"; shift iflags= +msys2_runtime_commit= # Find header file locations while [ -n "$*" ]; do case "$1" in @@ -26,6 +27,9 @@ while [ -n "$*" ]; do shift iflags="$iflags -I$1" ;; + -DMSYS2_RUNTIME_COMMIT=*) + msys2_runtime_commit="${1#*=}" + ;; esac shift done @@ -168,6 +172,10 @@ then cvs_tag="$(echo $wv_cvs_tag | sed -e 's/-branch.*//')" cygwin_ver="$cygwin_ver-$cvs_tag" fi +if [ -n "$msys2_runtime_commit" ] +then + cygwin_ver="$cygwin_ver-$msys2_runtime_commit" +fi echo "Version $cygwin_ver" set -$- $builddate diff --git a/winsup/cygwin/uname.cc b/winsup/cygwin/uname.cc index cca66be45a..8f984fac9b 100644 --- a/winsup/cygwin/uname.cc +++ b/winsup/cygwin/uname.cc @@ -91,18 +91,19 @@ uname_x (struct utsname *name) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-truncation=" #ifdef CYGPORT_RELEASE_INFO - snprintf (name->release, _UTSNAME_LENGTH, "%s.%s", - __XSTRING (CYGPORT_RELEASE_INFO), name->machine); + snprintf (name->release, _UTSNAME_LENGTH, "%s-%s.%s", + __XSTRING (CYGPORT_RELEASE_INFO), MSYS2_RUNTIME_COMMIT_SHORT, name->machine); #else extern const char *uname_dev_version; if (uname_dev_version && uname_dev_version[0]) - snprintf (name->release, _UTSNAME_LENGTH, "%s.%s", - uname_dev_version, name->machine); + snprintf (name->release, _UTSNAME_LENGTH, "%s-%s.%s", + uname_dev_version, MSYS2_RUNTIME_COMMIT_SHORT, name->machine); else - __small_sprintf (name->release, "%d.%d.%d-api-%d.%s", + __small_sprintf (name->release, "%d.%d.%d-%s-api-%d.%s", cygwin_version.dll_major / 1000, cygwin_version.dll_major % 1000, cygwin_version.dll_minor, + MSYS2_RUNTIME_COMMIT_SHORT, cygwin_version.api_minor, name->machine); #endif @@ -145,14 +146,15 @@ uname (struct utsname *in_name) cygwin_gethostname (name->nodename, sizeof (name->nodename) - 1); /* Cygwin dll release */ - __small_sprintf (name->release, "%d.%d.%d(%d.%d/%d/%d)", + __small_sprintf (name->release, "%d.%d.%d(%d.%d/%d/%d/%s)", cygwin_version.dll_major / 1000, cygwin_version.dll_major % 1000, cygwin_version.dll_minor, cygwin_version.api_major, cygwin_version.api_minor, cygwin_version.shared_data, - cygwin_version.mount_registry); + cygwin_version.mount_registry, + MSYS2_RUNTIME_COMMIT_SHORT); /* Cygwin "version" aka build date */ strcpy (name->version, cygwin_version.dll_build_date); From 3bf64eb9bbab012903357981977b84520ac2f11b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 22 May 2023 13:36:27 +0200 Subject: [PATCH 069/102] Cygwin: Adjust CWD magic to accommodate for the latest Windows previews Reportedly a very recent internal build of Windows 11 once again changed the current working directory logic a bit, and Cygwin's "magic" (or: "technologically sufficiently advanced") code needs to be adjusted accordingly. In particular, the following assembly code can be seen: ntdll!RtlpReferenceCurrentDirectory 598 00000001`800c6925 488d0db4cd0f00 lea rcx,[ntdll!FastPebLock (00000001`801c36e0)] 583 00000001`800c692c 4c897810 mov qword ptr [rax+10h],r15 588 00000001`800c6930 0f1140c8 movups xmmword ptr [rax-38h],xmm0 598 00000001`800c6934 e82774f4ff call ntdll!RtlEnterCriticalSection The change necessarily looks a bit different than 4840a56325 (Cygwin: Adjust CWD magic to accommodate for the latest Windows previews, 2023-05-22): The needle `\x48\x8d\x0d` is already present, as the first version of the hack after Windows 8.1 was released. In that code, though, the `call` to `RtlEnterCriticalSection` followed the `lea` instruction immediately, but now there are two more instructions separating them. Note: In the long run, we may very well want to follow the insightful suggestion by a helpful Windows kernel engineer who pointed out that it may be less fragile to implement kind of a disassembler that has a better chance to adapt to the ever-changing code of `ntdll!RtlpReferenceCurrentDirectory` by skipping uninteresting instructions such as `mov %rsp,%rax`, `mov %rbx,0x20(%rax)`, `push %rsi` `sub $0x70,%rsp`, etc, and focuses on finding the `lea`, `call ntdll!RtlEnterCriticalSection` and `mov ..., rbx` instructions, much like it was prototyped out for ARM64 at https://gist.github.com/jeremyd2019/aa167df0a0ae422fa6ebaea5b60c80c9 Signed-off-by: Johannes Schindelin --- winsup/cygwin/path.cc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/winsup/cygwin/path.cc b/winsup/cygwin/path.cc index 9618676b0b..bcde89c5ac 100644 --- a/winsup/cygwin/path.cc +++ b/winsup/cygwin/path.cc @@ -4899,6 +4899,18 @@ find_fast_cwd_pointer () %rcx for the subsequent RtlEnterCriticalSection call. */ lock = (const uint8_t *) memmem ((const char *) use_cwd, 80, "\x48\x8d\x0d", 3); + if (lock) + { + /* A recent Windows 11 Preview calls `lea rel(rip),%rcx' then + a `mov` and a `movups` instruction, and only then + `callq RtlEnterCriticalSection'. + */ + if (memmem (lock + 7, 8, "\x4c\x89\x78\x10\x0f\x11\x40\xc8", 8)) + { + call_rtl_offset = 15; + } + } + if (!lock) { /* Windows 8.1 Preview calls `lea rel(rip),%r12' then some unrelated From 2558ff9844fb0715e0bf72397a137421f30a1e8a Mon Sep 17 00:00:00 2001 From: Pip Cet Date: Thu, 12 Mar 2026 08:17:27 +0100 Subject: [PATCH 070/102] Cygwin: Fix segfault when XSAVE area sizes are unaligned During signal delivery, Cygwin saves the CPU's extended register state (floating-point, SSE, AVX, etc.) to a stack buffer using the xsave64 instruction, which requires its destination to be 64-byte aligned. Before executing xsave64, the code queries the CPU (via cpuid) for the required buffer size, then subtracts that size (plus a fixed overhead) from the stack pointer. The stack alignment arithmetic assumes that cpuid returns a size that is a multiple of 64. Until recently, this held true for all x86 CPUs. On recent AMD and Intel CPUs, however, the PKU feature (Protection Keys for Userspace, a memory-protection mechanism) adds an XSAVE component of only 8 bytes, which makes the total size no longer a multiple of 64. The subtraction then places the xsave64 buffer at a misaligned address, causing a segfault. This was first observed when running Cygwin/MSYS2 under Wine on Linux, where the host kernel exposes the PKU feature directly. The same problem could surface on future Windows versions that expose PKU or other small XSAVE components. The fix rounds up the cpuid-reported size to the next 64-byte multiple before using it in the stack allocation. The existing code already guarantees correct alignment for any buffer size that is a multiple of 64, so this rounding is sufficient. Fixes: c607889824 ("Cygwin: sigfe: Fix a bug that signal handler destroys fpu states") Signed-off-by: Pip Cet --- winsup/cygwin/scripts/gendef | 2 ++ 1 file changed, 2 insertions(+) diff --git a/winsup/cygwin/scripts/gendef b/winsup/cygwin/scripts/gendef index 861a2405b2..6328fe2fbd 100755 --- a/winsup/cygwin/scripts/gendef +++ b/winsup/cygwin/scripts/gendef @@ -233,6 +233,8 @@ sigdelayed: xorl %ecx,%ecx cpuid # get necessary space for xsave movq %rbx,%rcx + addq \$63, %rbx + andq \$-64, %rbx # align to next 64-byte multiple addq \$0x48,%rbx # 0x18 for alignment, 0x30 for additional space subq %rbx,%rsp movl %ebx,0x24(%rsp) From 9a6cbe55782d23ae684ef8e52abc3ef895e4d8df Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Mon, 18 May 2026 21:13:38 +0200 Subject: [PATCH 071/102] cygcheck: remove an unused variable causing a build error with GCC 16 Since gcc 16 this triggers -Werror=unused-but-set-variable= Remove the unused variable. --- winsup/utils/mingw/cygcheck.cc | 2 -- 1 file changed, 2 deletions(-) diff --git a/winsup/utils/mingw/cygcheck.cc b/winsup/utils/mingw/cygcheck.cc index 1637683c26..22c7d181ae 100644 --- a/winsup/utils/mingw/cygcheck.cc +++ b/winsup/utils/mingw/cygcheck.cc @@ -1706,7 +1706,6 @@ dump_sysinfo () else { char sep = strchr (s, ';') ? ';' : ':'; - int count_path_items = 0; while (1) { for (e = s; *e && *e != sep; e++); @@ -1714,7 +1713,6 @@ dump_sysinfo () printf ("\t%.*s\n", (int) (e - s), s); else puts ("\t."); - count_path_items++; if (!*e) break; s = e + 1; From 2105f6cc774dcf6aefca483f0356c8cdc389c49a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 24 Nov 2025 04:09:34 +0000 Subject: [PATCH 072/102] fixup! CI: add a GHA for doing a basic build test Upgrade GitHub Actions in `build.yaml`: `actions/upload-artifact` from v4 to v7 (the "Upload" step that publishes the just-built `_dest/` install tree as the `install` artifact) and `actions/download-artifact` from v4 to v8 (the "Download msys2-runtime artifact" step in the `msys2-tests` job that overlays that artifact on top of the runner's MSYS2 installation), as well as `checkout` to v7. These steps invoke the actions with default arguments, so the bumps are behaviorally transparent: they advance the Node.js runtime on the runners and retire the v4 deprecation warnings without altering the upload or download semantics. The asymmetric target versions (upload v7, download v8) simply reflect each action's then-current latest major release. Originally-authored-by: dependabot[bot] Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .github/workflows/build.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1057c35bea..4e219c3cea 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: setup-msys2 uses: msys2/setup-msys2@v2 @@ -33,7 +33,7 @@ jobs: make DESTDIR="$(pwd)"/_dest install - name: Upload - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: install path: _dest/ @@ -81,7 +81,7 @@ jobs: msys2 -c 'pacman --noconfirm -Suu' - name: Download msys2-runtime artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: install path: ${{ steps.msys2.outputs.msys2-location }} From a362d1ce29af6207c620421dca426ab4458710cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A7=88=EB=88=84=EC=97=98?= Date: Mon, 9 Mar 2015 16:24:43 +0100 Subject: [PATCH 073/102] fixup! Convert Unix paths in args/env to Windows form for native Win32 apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a non-ASCII character is at the beginning of a path, the current conversion destroys the path. This fix will prevent this with an extra check for non-ASCII UTF-8 characters. Helped-by: Johannes Schindelin Signed-off-by: 마누엘 --- winsup/cygwin/msys2_path_conv.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winsup/cygwin/msys2_path_conv.cc b/winsup/cygwin/msys2_path_conv.cc index 4c0cc82cf2..0dc086aae9 100644 --- a/winsup/cygwin/msys2_path_conv.cc +++ b/winsup/cygwin/msys2_path_conv.cc @@ -399,7 +399,7 @@ path_type find_path_start_and_type(const char** src, int recurse, const char* en } it = *src; - while (!isalnum(*it) && *it != '/' && *it != '\\' && *it != ':' && *it != '-' && *it != '.') { + while (!isalnum(*it) && !(0x80 & *it) && *it != '/' && *it != '\\' && *it != ':' && *it != '-' && *it != '.') { recurse = true; it = ++*src; if (it == end || *it == '\0') return NONE; From b0d4659e6592f84402961a39fe0a230614a02661 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 16 Jul 2026 15:18:45 +0200 Subject: [PATCH 074/102] Cygwin: pty: keep interactive console input for native programs via Cygwin Currently, when a native Windows program starts a Cygwin program while a pseudo console is active, and the Cygwin program then starts another native Windows program, the final program can lose access to console input. It then behaves as though its standard input were redirected instead of remaining interactive. For example, a native `git.exe` may invoke shell aliases (i.e. execute a shell command) that would in turn call interactive Git commands who would no longer work because their standard input appeared to be redirected. This can be demonstrated as follows: git -c 'alias.console-probe=!powershell.exe -NoLogo -NoProfile -Command " Write-Output ([Console]::IsInputRedirected) try { [void][Console]::KeyAvailable exit 0 } catch { exit 1 } "' console-probe Running this command with a Win32 version of `git.exe` currently prints `True` and exits with exit code 1. In the latest official release, where this bug is not present, it prints `False` and results in exit code 0. The reason is to be fonud in the archetype code. Reminder: For each pseudo terminal (pty), the archetype is the shared pty fhandler that owns the underlying native handles and supplies them to every per-file-descriptor fhandler for that pty. `open_with_arch()` calls `open()`, copies the first pty fhandler's state into the archetype, and then calls `open_setup()`. At that stage, pcon handle adoption already took place in `open_setup()`. This was not anticipated by 60a88896dc (Cygwin: pty: do not leak nat handles when adopting the pcon's in open_setup(), 2026-06-25), which tried to fix a leak by closing the superseded native handles as they were replaced in `open_setup()`. Because `open_with_arch()` had already copied those handle values into the archetype, closing them invalidated the archetype's copies. The archetype therefore retained stale values for those closed handles, which later pty fd fhandlers would inherit. If Windows reuses one of those values for a newly duplicated pcon handle, closing the stale value closes the new handle instead. The nested native program then receives unusable console input. Preserve usable console input by moving the unchanged transactional pcon handle adoption to `open()`, before the archetype snapshot. The archetype then receives valid pcon handles, all pty fd fhandlers inherit live handles, and the superseded raw pipe handles are closed exactly once. This commit is best viewed with `--color-moved`. Fixes: 60a88896dce0 ("Cygwin: pty: do not leak nat handles when adopting the pcon's in open_setup()") Assisted-by: GPT-5.6 Sol Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/pty.cc | 44 +++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index 2c28e7d8eb..ba726a061c 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -1059,26 +1059,6 @@ fhandler_pty_slave::open (int flags, mode_t) release_attach_mutex (); } - set_open_status (); - return 1; - -err: - if (GetLastError () == ERROR_FILE_NOT_FOUND) - set_errno (ENXIO); - else - __seterrno (); -err_no_errno: - termios_printf (errmsg); -err_no_msg: - for (HANDLE **h = handles; *h; h++) - if (**h && **h != INVALID_HANDLE_VALUE) - CloseHandle (**h); - return 0; -} - -bool -fhandler_pty_slave::open_setup (int flags) -{ if (get_ttyp ()->pcon_activated) { HANDLE pcon_owner = OpenProcess (PROCESS_DUP_HANDLE, FALSE, @@ -1094,8 +1074,8 @@ fhandler_pty_slave::open_setup (int flags) 0, TRUE, DUPLICATE_SAME_ACCESS); if (ok_in && ok_out) { - /* Close the cyg master-side handles open() installed before - replacing them, so they do not leak. */ + /* Replace these before open_with_arch() copies them into the + archetype shared by all pty slave fhandlers. */ CloseHandle (get_handle_nat ()); CloseHandle (get_output_handle_nat ()); set_handle_nat (new_in); @@ -1112,6 +1092,26 @@ fhandler_pty_slave::open_setup (int flags) } } + set_open_status (); + return 1; + +err: + if (GetLastError () == ERROR_FILE_NOT_FOUND) + set_errno (ENXIO); + else + __seterrno (); +err_no_errno: + termios_printf (errmsg); +err_no_msg: + for (HANDLE **h = handles; *h; h++) + if (**h && **h != INVALID_HANDLE_VALUE) + CloseHandle (**h); + return 0; +} + +bool +fhandler_pty_slave::open_setup (int flags) +{ set_flags ((flags & ~O_TEXT) | O_BINARY); myself->set_ctty (this, flags); report_tty_counts (this, "opened", ""); From 29862e4776ea00adc10d944851234c85e303b836 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 24 Nov 2025 04:09:34 +0000 Subject: [PATCH 075/102] Cygwin: CI: update Actions versions Update both versioned actions in `cygwin.yml` to current major versions: `actions/checkout` from v3 to v6 (in both invocations: the Fedora cross-build job and the windows-2022 native build job) and `actions/upload-artifact` from v4 to v6 (the "Upload test logs" step that publishes the testsuite `*.log` and `*.trs` artifacts). All three call sites use the actions with default arguments, so the bumps are behaviorally transparent: they exist purely to advance the underlying Node.js runtime to v24 on the GitHub Actions runners and to retire the v3/v4 deprecation warnings emitted on each workflow run. Cygwin upstream's `cygwin.yml` still pins the older v3 and v4 revisions. Carrying this bump in the Git for Windows fork keeps the fork's CI clean ahead of upstream catching up at the next merging-rebase. Originally-authored-by: dependabot[bot] Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .github/workflows/cygwin.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cygwin.yml b/.github/workflows/cygwin.yml index 998dc01576..a4222ebeb9 100644 --- a/.github/workflows/cygwin.yml +++ b/.github/workflows/cygwin.yml @@ -18,7 +18,7 @@ jobs: HAS_SSH_KEY: ${{ secrets.SSH_KEY != '' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 # install build tools - name: Install build tools @@ -105,7 +105,7 @@ jobs: run: | icacls . /inheritance:r icacls . /grant Administrators:F - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 # install cygwin and build tools - name: Install Cygwin @@ -168,7 +168,7 @@ jobs: # upload test logs to facilitate investigation of problems - name: Upload test logs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: testlogs path: | From 98101040305c24cb646cb00dbc253723c3833a80 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2015 11:54:47 +0000 Subject: [PATCH 076/102] Mention the extremely useful small_printf() function It came in real handy while debugging an issue that strace 'fixed'. Signed-off-by: Johannes Schindelin --- winsup/cygwin/DevDocs/how-to-debug-cygwin.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/winsup/cygwin/DevDocs/how-to-debug-cygwin.txt b/winsup/cygwin/DevDocs/how-to-debug-cygwin.txt index 61e91c88d5..953d375864 100644 --- a/winsup/cygwin/DevDocs/how-to-debug-cygwin.txt +++ b/winsup/cygwin/DevDocs/how-to-debug-cygwin.txt @@ -126,3 +126,9 @@ set CYGWIN_DEBUG=cat.exe:gdb.exe program will crash, probably in small_printf. At that point, a 'bt' command should show you the offending call to strace_printf with the improper format string. + +9. Debug output without strace + + If you cannot use gdb, or if the program behaves differently using strace + for whatever reason, you can still use the small_printf() function to + output debugging messages directly to stderr. From 52cca5be5f4a8c38fcef46af63eae6293ce2cbc7 Mon Sep 17 00:00:00 2001 From: Karsten Blees Date: Wed, 20 May 2015 16:32:52 +0200 Subject: [PATCH 077/102] Allow native symlinks to non-existing targets in 'nativestrict' mode Windows native symlinks must match the type of their target (file or directory), otherwise native Windows tools will fail. Creating symlinks in 'nativestrict' mode currently requires the target to exist in order to check its type. However, the target of a symlink can change at any time after the symlink has been created. Thus users of native symlinks must be prepared to deal with type mismatches anyway. Checking the target type at symlink creation time is not a good reason to violate the symlink() API specification. In 'nativestrict' mode, always create native symlinks. Choose the symlink type according to the target if it exists. Otherwise check the target path for a trailing '/' as hint to create a directory symlink. This allows callers to explicitly specify the expected target type, e.g.: $ ln -s test/ link-to-test $ mkdir test Signed-off-by: Karsten Blees Signed-off-by: Johannes Schindelin --- winsup/cygwin/path.cc | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/winsup/cygwin/path.cc b/winsup/cygwin/path.cc index bcde89c5ac..a85e884417 100644 --- a/winsup/cygwin/path.cc +++ b/winsup/cygwin/path.cc @@ -2014,7 +2014,7 @@ symlink_native (const char *oldpath, path_conv &win32_newpath) path_conv win32_oldpath; PUNICODE_STRING final_oldpath, final_newpath; UNICODE_STRING final_oldpath_buf; - DWORD flags; + DWORD flags = 0; if (resolve_symlink_target (oldpath, win32_newpath, win32_oldpath)) final_oldpath = win32_oldpath.get_nt_native_path (); @@ -2076,14 +2076,39 @@ symlink_native (const char *oldpath, path_conv &win32_newpath) wcpcpy (e_old, c_old); } } - /* If the symlink target doesn't exist, don't create native symlink. - Otherwise the directory flag in the symlink is potentially wrong - when the target comes into existence, and native tools will fail. - This is so screwball. This is no problem on AFS, fortunately. */ - if (!win32_oldpath.exists () && !win32_oldpath.fs_is_afs ()) + + /* The directory flag in the symlink must match the target type, + otherwise native tools will fail (fortunately this is no problem + on AFS). Do our best to guess the symlink type correctly. */ + if (win32_oldpath.exists () || win32_oldpath.fs_is_afs ()) { - SetLastError (ERROR_FILE_NOT_FOUND); - return -1; + /* If the target exists (or on AFS), check the target type. Note + that this may still be wrong if the target is changed after + creating the symlink (e.g. in bulk operations such as rsync, + unpacking archives or VCS checkouts). */ + if (win32_oldpath.isdir ()) + flags |= SYMBOLIC_LINK_FLAG_DIRECTORY; + } + else + { + if (allow_winsymlinks == WSYM_nativestrict) + { + /* In nativestrict mode, if the target does not exist, use + trailing '/' in the target path as hint to create a + directory symlink. */ + ssize_t len = strlen(oldpath); + if (len && isdirsep(oldpath[len - 1])) + flags |= SYMBOLIC_LINK_FLAG_DIRECTORY; + } + else + { + /* In native mode, if the target does not exist, fall back + to creating a Cygwin symlink file (or in case of MSys: + try to copy the (non-existing) target, which will of + course fail). */ + SetLastError (ERROR_FILE_NOT_FOUND); + return -1; + } } /* Don't allow native symlinks to Cygwin special files. However, the caller shoud know because this case shouldn't be covered by the @@ -2112,7 +2137,6 @@ symlink_native (const char *oldpath, path_conv &win32_newpath) final_oldpath->Buffer[1] = L'\\'; } /* Try to create native symlink. */ - flags = win32_oldpath.isdir () ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0; if (wincap.has_unprivileged_createsymlink ()) flags |= SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; if (!CreateSymbolicLinkW (final_newpath->Buffer, final_oldpath->Buffer, From e72f778ec31bc4c69f6853826c773a7b2f6f18a0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2015 13:56:22 +0000 Subject: [PATCH 078/102] WIP Handle 8-bit characters under LOCALE=C TODO!!! Verify that this is still needed, as it seems to be no longer necessary to pass Git's test suite because the official MSYS2 runtime lacks this patch yet is sufficient to let Git's test suite succeed. Even when the character set is specified as ASCII, we should handle data outside the 7-bit range gracefully by simply copying it, even if it is technically no longer ASCII. This fixes several of Git for Windows' tests, e.g. t7400. Signed-off-by: Johannes Schindelin --- winsup/cygwin/strfuncs.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/winsup/cygwin/strfuncs.cc b/winsup/cygwin/strfuncs.cc index 0cf41cefc8..ced11f0f7f 100644 --- a/winsup/cygwin/strfuncs.cc +++ b/winsup/cygwin/strfuncs.cc @@ -1196,7 +1196,11 @@ _sys_mbstowcs (mbtowc_p f_mbtowc, wchar_t *dst, size_t dlen, const char *src, { bytes = 1; if (dst) +#ifdef STRICTLY_7BIT_ASCII *ptr = L'\xf000' | *pmbs; +#else + *ptr = *pmbs; +#endif } memset (&ps, 0, sizeof ps); } From de839b5b9510cb7399009cc3fc754d12c95fe361 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 18 Dec 2015 20:19:57 +0100 Subject: [PATCH 079/102] Make paths' WCS->MBS conversion explicit * dcrt0.cc (dll_crt0_1), dtable.cc (handle_to_fn), environ.cc (environ_init, getwinenveq, build_env), external.cc (fillout_pinfo), fhandler_disk_file.cc (__DIR_mounts::eval_ino, fhandler_disk_file::readdir_helper), fhandler_netdrive.cc (fhandler_netdrive::readdir), fhandler_process.cc (format_process_winexename, format_process_maps, format_process_stat, format_process_status), fhandler_procsys.cc (fill_filebuf, fhandler_procsys::readdir), mount.cc (fs_info::update, mount_info::create_root_entry, mount_info::conv_to_posix_path, mount_info::from_fstab_line), nlsfuncs.cc (internal_setlocale), path.cc (path_conv::check, sysmlink_info::check_shortcut, symlink_info::check_sysfile, symlink_info::check_reparse_point, symlink_info::check_nfs_symlink, cygwin_conv_path, cygwin_conv_path_list, cwdstuff::get_error_desc, cwdstuff::get), strfuncs.cc (sys_wcstombs_no_path, sys_wcstombs_alloc_no_path), uinfo.cc (ontherange, fetch_from_path, cygheap_pwdgrp::get_home, cygheap_pwdgrp::get_shell, cygheap_pwdgrp::get_gecos), wchar.h (sys_wcstombs_no_path, sys_wcstombs_alloc_no_path): Convert call sites of the sys_wcstombs*() family to specify explicitly when the parameter refers to a path or file name, to avoid future misconversions. Detailed explanation: The sys_wcstombs() function contains special handling for paths/file names, to work around file name restriction on Windows that are unexpected in the POSIX context of Cygwin. We actually do not want that special handling for WCS strings that do *not* refer to paths or file names. Neither do we want to convert those special file names unless they come from inside Cygwin: if the source of the string value is the Windows API, we *know* it cannot be such a special file name because Windows itself would not be able to handle it in the way Cygwin does. So let's switch the previous sys_wcstombs()/sys_wcstombs_no_path() (and the *_alloc* variant) around to sys_wcstombs_path()/sys_wcstombs(). We do this for several reasons: - whenever a call site wants to convert a WCS representation of a path or file name to an MBS one, it should be made very clear that we *want* the special file name conversion to happen. - it is shorter to read and write. - future calls to sys_wcstombs() will not incur unwanted conversion by accident (it is easy for unsuspecting programmers to assume that the function name "sys_wcstombs()" refers to a regular text conversion that has nothing to do with paths or filenames). By keeping the name sys_wcstombs() (and not switching to sys_wcstombs_path()), the following call sites are implicitly changed to *exclude* the special path/file name conversion: cygheap.h (get_drive): Cannot contain special characters external.cc (cygwin_internal): Refers to user/domain names, not paths fhandler_clipboard.cc (fhandler_dev_clipboard::read): Is not a path or file name but characters from the Windows clipboard fhandler_console.cc: (dev_console::con_to_str): Is not a path or file name but characters from the console fhandler_registry.cc (encode_regname): Is a registry key, not a path or filename fhandler_registry.cc (multi_wcstombs): All call sites pass registry values, not paths or filenames fhandler_registry.cc (fstat): Is a registry value, not a path or filename fhandler_registry.cc (fill_filebuf): Is a registry value, not a path or filename net.cc (get_ipv4fromreg): Is a registry value, not a path or filename net.cc (get_friendlyname): Is a device name, not a path or filename netdb.cc (open_system_file): Is from outside Cygwin smallprint.cc (__small_vsprintf): Is a free text, not a path or filename strfuncs.cc (strlwr): Should preserve the characters from the private page if there are any strfuncs.cc (strupr): Should preserve the characters from the private page if there are any uinfo.cc (cygheap_user::init): Refers to a user name, not a path or filename uinfo.cc (pwdgrp::fetch_account_from_windows): Refers to value from outside Cygwin By keeping the function name sys_wcstombs_alloc() (and not changing it to sys_wcstombs_alloc_path()), the following call sites are implicitly changed to *exclude* the special path/file name conversion: ldap.cc (cyg_ldap::remap_uid): Refers to a user name, not a path or filename ldap.cc (cyg_ldap::remap_gid): Refers to a group name, not a path or filename pinfo.cc (_pinfo::cmdline): Refers to a command line from Windows, outside Cygwin uinfo.cc (cygheap_user::env_logsrv): Is a server name, not a path or filename uinfo.cc (cygheap_user::env_domain): Refers to the user/domain name, not a path or filename uinfo.cc (cygheap_user::env_userprofile): Refers to Windows' idea of a path, outside Cygwin uinfo.cc (cygheap_user::env_systemroot): Refers to Windows' idea of a path, outside Cygwin uinfo.cc (fetch_from_description): Refers to values from outside of Cygwin uinfo.cc (cygheap_pwdgrp::get_gecos): Refers to user/domain name and email address, not path nor filename Signed-off-by: Johannes Schindelin --- winsup/cygwin/dcrt0.cc | 4 ++-- winsup/cygwin/dtable.cc | 2 +- winsup/cygwin/environ.cc | 8 ++++---- winsup/cygwin/external.cc | 2 +- winsup/cygwin/fhandler/disk_file.cc | 4 ++-- winsup/cygwin/fhandler/netdrive.cc | 2 +- winsup/cygwin/fhandler/process.cc | 11 ++++++----- winsup/cygwin/fhandler/procsys.cc | 11 ++++++----- winsup/cygwin/local_includes/wchar.h | 16 +++++++-------- winsup/cygwin/mount.cc | 10 +++++----- winsup/cygwin/nlsfuncs.cc | 2 +- winsup/cygwin/path.cc | 29 ++++++++++++++-------------- winsup/cygwin/uinfo.cc | 20 +++++++++---------- 13 files changed, 62 insertions(+), 59 deletions(-) diff --git a/winsup/cygwin/dcrt0.cc b/winsup/cygwin/dcrt0.cc index 33cad1e3fe..61ecb757a2 100644 --- a/winsup/cygwin/dcrt0.cc +++ b/winsup/cygwin/dcrt0.cc @@ -912,9 +912,9 @@ dll_crt0_1 (void *) if (!__argc) { PWCHAR wline = GetCommandLineW (); - size_t size = sys_wcstombs_no_path (NULL, 0, wline) + 1; + size_t size = sys_wcstombs (NULL, 0, wline) + 1; char *line = (char *) alloca (size); - sys_wcstombs_no_path (line, size, wline); + sys_wcstombs (line, size, wline); /* Scan the command line and build argv. Expand wildcards if not called from another cygwin process. */ diff --git a/winsup/cygwin/dtable.cc b/winsup/cygwin/dtable.cc index 694ec81e22..3312de9373 100644 --- a/winsup/cygwin/dtable.cc +++ b/winsup/cygwin/dtable.cc @@ -1037,7 +1037,7 @@ handle_to_fn (HANDLE h, char *posix_fn) if (wcsncasecmp (w32, DEVICE_PREFIX, DEVICE_PREFIX_LEN) != 0 || !QueryDosDeviceW (NULL, fnbuf, sizeof (fnbuf) / sizeof (WCHAR))) { - sys_wcstombs (posix_fn, NT_MAX_PATH, w32, w32len); + sys_wcstombs_path (posix_fn, NT_MAX_PATH, w32, w32len); return false; } diff --git a/winsup/cygwin/environ.cc b/winsup/cygwin/environ.cc index 4e049211e9..529535a580 100644 --- a/winsup/cygwin/environ.cc +++ b/winsup/cygwin/environ.cc @@ -918,7 +918,7 @@ win32env_to_cygenv (PWCHAR rawenv, bool posify) eventually want to use them). */ for (i = 0, w = rawenv; *w != L'\0'; w = wcschr (w, L'\0') + 1, i++) { - sys_wcstombs_alloc_no_path (&newp, HEAP_NOTHEAP, w); + sys_wcstombs_alloc (&newp, HEAP_NOTHEAP, w); if (i >= envc) envp = (char **) realloc (envp, (4 + (envc += 100)) * sizeof (char *)); envp[i] = newp; @@ -978,7 +978,7 @@ getwinenveq (const char *name, size_t namelen, int x) int totlen = GetEnvironmentVariableW (name0, valbuf, 32768); if (totlen > 0) { - totlen = sys_wcstombs_no_path (NULL, 0, valbuf) + 1; + totlen = sys_wcstombs (NULL, 0, valbuf) + 1; if (x == HEAP_1_STR) totlen += namelen; else @@ -986,7 +986,7 @@ getwinenveq (const char *name, size_t namelen, int x) char *p = (char *) cmalloc_abort ((cygheap_types) x, totlen); if (namelen) strcpy (p, name); - sys_wcstombs_no_path (p + namelen, totlen, valbuf); + sys_wcstombs (p + namelen, totlen, valbuf); debug_printf ("using value from GetEnvironmentVariable for '%W'", name0); return p; } @@ -1144,7 +1144,7 @@ build_env (const char * const *envp, PWCHAR &envblock, int &envc, for (winnum = 0, var = cwinenv; *var; ++winnum, var = wcschr (var, L'\0') + 1) - sys_wcstombs_alloc_no_path (&winenv[winnum], HEAP_NOTHEAP, var); + sys_wcstombs_alloc (&winenv[winnum], HEAP_NOTHEAP, var); } DestroyEnvironmentBlock (cwinenv); /* Eliminate variables which are already available in envp, as well as diff --git a/winsup/cygwin/external.cc b/winsup/cygwin/external.cc index a20ea078e3..fa9d696ad0 100644 --- a/winsup/cygwin/external.cc +++ b/winsup/cygwin/external.cc @@ -92,7 +92,7 @@ fillout_pinfo (pid_t pid, int winpid) ep.rusage_self = p->rusage_self; ep.rusage_children = p->rusage_children; ep.progname[0] = '\0'; - sys_wcstombs(ep.progname, MAX_PATH, p->progname); + sys_wcstombs_path (ep.progname, MAX_PATH, p->progname); ep.strace_mask = 0; ep.version = EXTERNAL_PINFO_VERSION; diff --git a/winsup/cygwin/fhandler/disk_file.cc b/winsup/cygwin/fhandler/disk_file.cc index d54d3747ea..caf69f808a 100644 --- a/winsup/cygwin/fhandler/disk_file.cc +++ b/winsup/cygwin/fhandler/disk_file.cc @@ -2403,7 +2403,7 @@ fhandler_disk_file::readdir_helper (DIR *dir, dirent *de, DWORD w32_err, char *p = stpcpy (file, pc.get_posix ()); if (p[-1] != '/') *p++ = '/'; - sys_wcstombs (p, NT_MAX_PATH - (p - file), + sys_wcstombs_path (p, NT_MAX_PATH - (p - file), fname->Buffer, fname->Length / sizeof (WCHAR)); path_conv fpath (file, PC_SYM_NOFOLLOW); if (fpath.issymlink ()) @@ -2424,7 +2424,7 @@ fhandler_disk_file::readdir_helper (DIR *dir, dirent *de, DWORD w32_err, } } - sys_wcstombs (de->d_name, NAME_MAX + 1, fname->Buffer, + sys_wcstombs_path (de->d_name, NAME_MAX + 1, fname->Buffer, fname->Length / sizeof (WCHAR)); /* Don't try to optimize relative to dir->__d_position. On several diff --git a/winsup/cygwin/fhandler/netdrive.cc b/winsup/cygwin/fhandler/netdrive.cc index 426542fe21..4f732b6d95 100644 --- a/winsup/cygwin/fhandler/netdrive.cc +++ b/winsup/cygwin/fhandler/netdrive.cc @@ -648,7 +648,7 @@ fhandler_netdrive::readdir (DIR *dir, dirent *de) goto out; } - sys_wcstombs (de->d_name, sizeof de->d_name, DIR_cache[dir->__d_position]); + sys_wcstombs_path (de->d_name, sizeof de->d_name, DIR_cache[dir->__d_position]); if (strlen (dir->__d_dirname) == 2) de->d_ino = hash_path_name (get_ino (), de->d_name); else diff --git a/winsup/cygwin/fhandler/process.cc b/winsup/cygwin/fhandler/process.cc index e00cae58d7..1eacee3fa4 100644 --- a/winsup/cygwin/fhandler/process.cc +++ b/winsup/cygwin/fhandler/process.cc @@ -578,10 +578,10 @@ static off_t format_process_winexename (void *data, char *&destbuf) { _pinfo *p = (_pinfo *) data; - size_t len = sys_wcstombs (NULL, 0, p->progname); + size_t len = sys_wcstombs_path (NULL, 0, p->progname); destbuf = (char *) crealloc_abort (destbuf, len + 1); /* With trailing \0 for backward compat reasons. */ - sys_wcstombs (destbuf, len + 1, p->progname); + sys_wcstombs_path (destbuf, len + 1, p->progname); return len; } @@ -1082,7 +1082,7 @@ format_process_maps (void *data, char *&destbuf) drive_maps.fixup_if_match (msi->SectionFileName.Buffer); if (mount_table->conv_to_posix_path (dosname, posix_modname, 0)) - sys_wcstombs (posix_modname, NT_MAX_PATH, dosname); + sys_wcstombs_path (posix_modname, NT_MAX_PATH, dosname); stat (posix_modname, &st); } else if (!threads.fill_if_match (cur.abase, mb.Type, @@ -1138,7 +1138,7 @@ format_process_stat (void *data, char *&destbuf) else { PWCHAR last_slash = wcsrchr (p->progname, L'\\'); - sys_wcstombs (cmd, NAME_MAX + 1, + sys_wcstombs_path (cmd, NAME_MAX + 1, last_slash ? last_slash + 1 : p->progname); int len = strlen (cmd); if (len > 4) @@ -1266,7 +1266,8 @@ format_process_status (void *data, char *&destbuf) bool fetch_siginfo = false; PWCHAR last_slash = wcsrchr (p->progname, L'\\'); - sys_wcstombs (cmd, NAME_MAX + 1, last_slash ? last_slash + 1 : p->progname); + sys_wcstombs_path (cmd, NAME_MAX + 1, + last_slash ? last_slash + 1 : p->progname); int len = strlen (cmd); if (len > 4) { diff --git a/winsup/cygwin/fhandler/procsys.cc b/winsup/cygwin/fhandler/procsys.cc index aa021e89c7..832434bb82 100644 --- a/winsup/cygwin/fhandler/procsys.cc +++ b/winsup/cygwin/fhandler/procsys.cc @@ -236,10 +236,11 @@ fhandler_procsys::fill_filebuf () NtClose (h); if (!NT_SUCCESS (status)) goto unreadable; - len = sys_wcstombs (NULL, 0, target.Buffer, target.Length / sizeof (WCHAR)); + len = sys_wcstombs_path (NULL, 0, + target.Buffer, target.Length / sizeof (WCHAR)); filebuf = (char *) crealloc_abort (filebuf, procsys_len + len + 1); - sys_wcstombs (fnamep = stpcpy (filebuf, procsys), len + 1, target.Buffer, - target.Length / sizeof (WCHAR)); + sys_wcstombs_path (fnamep = stpcpy (filebuf, procsys), len + 1, + target.Buffer, target.Length / sizeof (WCHAR)); while ((fnamep = strchr (fnamep, '\\'))) *fnamep = '/'; return true; @@ -377,8 +378,8 @@ fhandler_procsys::readdir (DIR *dir, dirent *de) res = ENMFILE; else { - sys_wcstombs (de->d_name, NAME_MAX + 1, dbi->ObjectName.Buffer, - dbi->ObjectName.Length / sizeof (WCHAR)); + sys_wcstombs_path (de->d_name, NAME_MAX + 1, dbi->ObjectName.Buffer, + dbi->ObjectName.Length / sizeof (WCHAR)); de->d_ino = hash_path_name (get_ino (), de->d_name); if (RtlEqualUnicodeString (&dbi->ObjectTypeName, &ro_u_natdir, FALSE)) de->d_type = DT_DIR; diff --git a/winsup/cygwin/local_includes/wchar.h b/winsup/cygwin/local_includes/wchar.h index 606559a6ab..c6ec5d8758 100644 --- a/winsup/cygwin/local_includes/wchar.h +++ b/winsup/cygwin/local_includes/wchar.h @@ -173,29 +173,29 @@ extern size_t _sys_wcstombs_alloc (char **dst_p, int type, const wchar_t *src, size_t nwc, bool is_path); static inline size_t -sys_wcstombs (char *dst, size_t len, const wchar_t * src, - size_t nwc = (size_t) -1) +sys_wcstombs_path (char *dst, size_t len, const wchar_t * src, + size_t nwc = (size_t) -1) { return _sys_wcstombs (dst, len, src, nwc, true); } static inline size_t -sys_wcstombs_no_path (char *dst, size_t len, const wchar_t * src, - size_t nwc = (size_t) -1) +sys_wcstombs (char *dst, size_t len, const wchar_t * src, + size_t nwc = (size_t) -1) { return _sys_wcstombs (dst, len, src, nwc, false); } static inline size_t -sys_wcstombs_alloc (char **dst_p, int type, const wchar_t *src, - size_t nwc = (size_t) -1) +sys_wcstombs_alloc_path (char **dst_p, int type, const wchar_t *src, + size_t nwc = (size_t) -1) { return _sys_wcstombs_alloc (dst_p, type, src, nwc, true); } static inline size_t -sys_wcstombs_alloc_no_path (char **dst_p, int type, const wchar_t *src, - size_t nwc = (size_t) -1) +sys_wcstombs_alloc (char **dst_p, int type, const wchar_t *src, + size_t nwc = (size_t) -1) { return _sys_wcstombs_alloc (dst_p, type, src, nwc, false); } diff --git a/winsup/cygwin/mount.cc b/winsup/cygwin/mount.cc index ff0279336b..ceb3864aab 100644 --- a/winsup/cygwin/mount.cc +++ b/winsup/cygwin/mount.cc @@ -506,7 +506,7 @@ fs_info::update (PUNICODE_STRING upath, HANDLE in_vol) { /* The filesystem name is only used in fillout_mntent and only if the filesystem isn't one of the well-known filesystems anyway. */ - sys_wcstombs (fsn, sizeof fsn, ffai_buf.ffai.FileSystemName, + sys_wcstombs_path (fsn, sizeof fsn, ffai_buf.ffai.FileSystemName, ffai_buf.ffai.FileSystemNameLength / sizeof (WCHAR)); strlwr (fsn); } @@ -547,7 +547,7 @@ mount_info::create_root_entry (const PWCHAR root) /* Create a default root dir derived from the location of the Cygwin DLL. The entry is immutable, unless the "override" option is given in /etc/fstab. */ char native_root[PATH_MAX]; - sys_wcstombs (native_root, PATH_MAX, root); + sys_wcstombs_path (native_root, PATH_MAX, root); assert (*native_root != '\0'); if (add_item (native_root, "/", MOUNT_SYSTEM | MOUNT_IMMUTABLE | MOUNT_AUTOMATIC | MOUNT_NOACL) @@ -941,7 +941,7 @@ mount_info::conv_to_posix_path (PWCHAR src_path, char *posix_path, } tmp_pathbuf tp; char *buf = tp.c_get (); - sys_wcstombs (buf, NT_MAX_PATH, src_path); + sys_wcstombs_path (buf, NT_MAX_PATH, src_path); int ret = conv_to_posix_path (buf, posix_path, ccp_flags); if (changed) src_path[0] = L'C'; @@ -1275,7 +1275,7 @@ mount_info::from_fstab_line (char *line, bool user) { tmp_pathbuf tp; char *mb_tmp = tp.c_get (); - sys_wcstombs (mb_tmp, PATH_MAX, tmp); + sys_wcstombs_path (mb_tmp, PATH_MAX, tmp); mount_flags |= MOUNT_USER_TEMP; int res = mount_table->add_item (mb_tmp, posix_path, mount_flags); @@ -1890,7 +1890,7 @@ mount_info::cygdrive_getmntent () if (wide_path) { win32_path = tp.c_get (); - sys_wcstombs (win32_path, NT_MAX_PATH, wide_path); + sys_wcstombs_path (win32_path, NT_MAX_PATH, wide_path); posix_path = tp.c_get (); cygdrive_posix_path (win32_path, posix_path, 0); return fillout_mntent (win32_path, posix_path, cygdrive_flags); diff --git a/winsup/cygwin/nlsfuncs.cc b/winsup/cygwin/nlsfuncs.cc index f57465a4f2..57af967c1a 100644 --- a/winsup/cygwin/nlsfuncs.cc +++ b/winsup/cygwin/nlsfuncs.cc @@ -1776,7 +1776,7 @@ internal_setlocale () if (w_path) { char *c_path = tp.c_get (); - sys_wcstombs (c_path, 32768, w_path); + sys_wcstombs_path (c_path, 32768, w_path); setenv ("PATH", c_path, 1); } } diff --git a/winsup/cygwin/path.cc b/winsup/cygwin/path.cc index bcde89c5ac..e97ae807a8 100644 --- a/winsup/cygwin/path.cc +++ b/winsup/cygwin/path.cc @@ -653,7 +653,8 @@ path_conv::check (const UNICODE_STRING *src, unsigned opt, char *path = tp.c_get (); user_shared->warned_msdos = true; - sys_wcstombs (path, NT_MAX_PATH, src->Buffer, src->Length / sizeof (WCHAR)); + sys_wcstombs_path (path, NT_MAX_PATH, + src->Buffer, src->Length / sizeof (WCHAR)); path_conv::check (path, opt, suffixes); } @@ -2695,7 +2696,7 @@ symlink_info::check_shortcut (HANDLE h) if (*(PWCHAR) cp == 0xfeff) /* BOM */ { char *tmpbuf = tp.c_get (); - if (sys_wcstombs (tmpbuf, NT_MAX_PATH, (PWCHAR) (cp + 2)) + if (sys_wcstombs_path (tmpbuf, NT_MAX_PATH, (PWCHAR) (cp + 2)) > SYMLINK_MAX) return 0; res = posixify (tmpbuf); @@ -2776,7 +2777,7 @@ symlink_info::check_sysfile (HANDLE h) else srcbuf += 2; char *tmpbuf = tp.c_get (); - if (sys_wcstombs (tmpbuf, NT_MAX_PATH, (PWCHAR) srcbuf) + if (sys_wcstombs_path (tmpbuf, NT_MAX_PATH, (PWCHAR) srcbuf) > SYMLINK_MAX) debug_printf ("symlink string too long"); else @@ -3044,8 +3045,8 @@ symlink_info::check_reparse_point (HANDLE h, bool remote) path_flags (path_flags () | ret); if (ret & PATH_SYMLINK) { - sys_wcstombs (srcbuf, SYMLINK_MAX + 7, symbuf.Buffer, - symbuf.Length / sizeof (WCHAR)); + sys_wcstombs_path (srcbuf, SYMLINK_MAX + 7, symbuf.Buffer, + symbuf.Length / sizeof (WCHAR)); /* A symlink is never a directory. */ fileattr (fileattr () & ~FILE_ATTRIBUTE_DIRECTORY); return posixify (srcbuf); @@ -3079,7 +3080,7 @@ symlink_info::check_nfs_symlink (HANDLE h) { PWCHAR spath = (PWCHAR) (pffei->EaName + pffei->EaNameLength + 1); - res = sys_wcstombs (contents, SYMLINK_MAX + 1, + res = sys_wcstombs_path (contents, SYMLINK_MAX + 1, spath, pffei->EaValueLength); path_flags (path_flags () | PATH_SYMLINK); } @@ -4278,7 +4279,7 @@ cygwin_conv_path (cygwin_conv_path_t what, const void *from, void *to, } PUNICODE_STRING up = p.get_nt_native_path (); buf = tp.c_get (); - sys_wcstombs (buf, NT_MAX_PATH, + sys_wcstombs_path (buf, NT_MAX_PATH, up->Buffer, up->Length / sizeof (WCHAR)); /* Convert native path to standard DOS path. */ if (!strncmp (buf, "\\??\\", 4)) @@ -4291,11 +4292,11 @@ cygwin_conv_path (cygwin_conv_path_t what, const void *from, void *to, { /* Device name points to somewhere else in the NT namespace. Use GLOBALROOT prefix to convert to Win32 path. */ - char *p = buf + sys_wcstombs (buf, NT_MAX_PATH, + char *p = buf + sys_wcstombs_path (buf, NT_MAX_PATH, ro_u_globalroot.Buffer, ro_u_globalroot.Length / sizeof (WCHAR)); - sys_wcstombs (p, NT_MAX_PATH - (p - buf), + sys_wcstombs_path (p, NT_MAX_PATH - (p - buf), up->Buffer, up->Length / sizeof (WCHAR)); } lsiz = strlen (buf) + 1; @@ -4607,8 +4608,8 @@ cygwin_conv_path_list (cygwin_conv_path_t what, const void *from, void *to, switch (what & CCP_CONVTYPE_MASK) { case CCP_WIN_W_TO_POSIX: - if (!sys_wcstombs_alloc (&winp, HEAP_NOTHEAP, (const wchar_t *) from, - (size_t) -1)) + if (!sys_wcstombs_alloc_path (&winp, HEAP_NOTHEAP, + (const wchar_t *) from, (size_t) -1)) return -1; what = (what & ~CCP_CONVTYPE_MASK) | CCP_WIN_A_TO_POSIX; from = (const void *) winp; @@ -5394,9 +5395,9 @@ cwdstuff::get_error_desc () const void cwdstuff::reset_posix (wchar_t *w_cwd) { - size_t len = sys_wcstombs (NULL, (size_t) -1, w_cwd); + size_t len = sys_wcstombs_path (NULL, (size_t) -1, w_cwd); posix = (char *) crealloc_abort (posix, len + 1); - sys_wcstombs (posix, len + 1, w_cwd); + sys_wcstombs_path (posix, len + 1, w_cwd); } char * @@ -5421,7 +5422,7 @@ cwdstuff::get (char *buf, int need_posix, int with_chroot, unsigned ulen) if (!need_posix) { tocopy = tp.c_get (); - sys_wcstombs (tocopy, NT_MAX_PATH, win32.Buffer, + sys_wcstombs_path (tocopy, NT_MAX_PATH, win32.Buffer, win32.Length / sizeof (WCHAR)); } else diff --git a/winsup/cygwin/uinfo.cc b/winsup/cygwin/uinfo.cc index 3d3b804c83..addaed13b1 100644 --- a/winsup/cygwin/uinfo.cc +++ b/winsup/cygwin/uinfo.cc @@ -391,12 +391,12 @@ cygheap_user::ontherange (homebodies what, struct passwd *pw) { if (ui->usri3_home_dir_drive && *ui->usri3_home_dir_drive) { - sys_wcstombs (homepath_env_buf, NT_MAX_PATH, + sys_wcstombs_path (homepath_env_buf, NT_MAX_PATH, ui->usri3_home_dir_drive); strcat (homepath_env_buf, "\\"); } else if (ui->usri3_home_dir && *ui->usri3_home_dir) - sys_wcstombs (homepath_env_buf, NT_MAX_PATH, + sys_wcstombs_path (homepath_env_buf, NT_MAX_PATH, ui->usri3_home_dir); } if (ui) @@ -406,7 +406,7 @@ cygheap_user::ontherange (homebodies what, struct passwd *pw) if (!homepath_env_buf[0] && get_user_profile_directory (get_windows_id (win_id), profile, MAX_PATH)) - sys_wcstombs (homepath_env_buf, NT_MAX_PATH, profile); + sys_wcstombs_path (homepath_env_buf, NT_MAX_PATH, profile); /* Last fallback: Cygwin root dir. */ if (!homepath_env_buf[0]) cygwin_conv_path (CCP_POSIX_TO_WIN_A | CCP_ABSOLUTE, @@ -925,7 +925,7 @@ fetch_from_path (cyg_ldap *pldap, PUSER_INFO_3 ui, cygpsid &sid, PCWSTR str, } } *w = L'\0'; - sys_wcstombs_alloc (&ret, HEAP_NOTHEAP, wpath); + sys_wcstombs_alloc_path (&ret, HEAP_NOTHEAP, wpath); return ret; } @@ -993,7 +993,7 @@ cygheap_pwdgrp::get_home (cyg_ldap *pldap, cygpsid &sid, PCWSTR dom, { val = pldap->get_string_attribute (L"cygwinHome"); if (val && *val) - sys_wcstombs_alloc (&home, HEAP_NOTHEAP, val); + sys_wcstombs_alloc_path (&home, HEAP_NOTHEAP, val); } break; case NSS_SCHEME_UNIX: @@ -1001,7 +1001,7 @@ cygheap_pwdgrp::get_home (cyg_ldap *pldap, cygpsid &sid, PCWSTR dom, { val = pldap->get_string_attribute (L"unixHomeDirectory"); if (val && *val) - sys_wcstombs_alloc (&home, HEAP_NOTHEAP, val); + sys_wcstombs_alloc_path (&home, HEAP_NOTHEAP, val); } break; case NSS_SCHEME_DESC: @@ -1026,7 +1026,7 @@ cygheap_pwdgrp::get_home (cyg_ldap *pldap, cygpsid &sid, PCWSTR dom, home = (char *) cygwin_create_path (CCP_WIN_W_TO_POSIX, val); else - sys_wcstombs_alloc (&home, HEAP_NOTHEAP, val); + sys_wcstombs_alloc_path (&home, HEAP_NOTHEAP, val); } } break; @@ -1096,7 +1096,7 @@ cygheap_pwdgrp::get_shell (cyg_ldap *pldap, cygpsid &sid, PCWSTR dom, { val = pldap->get_string_attribute (L"cygwinShell"); if (val && *val) - sys_wcstombs_alloc (&shell, HEAP_NOTHEAP, val); + sys_wcstombs_alloc_path (&shell, HEAP_NOTHEAP, val); } break; case NSS_SCHEME_UNIX: @@ -1104,7 +1104,7 @@ cygheap_pwdgrp::get_shell (cyg_ldap *pldap, cygpsid &sid, PCWSTR dom, { val = pldap->get_string_attribute (L"loginShell"); if (val && *val) - sys_wcstombs_alloc (&shell, HEAP_NOTHEAP, val); + sys_wcstombs_alloc_path (&shell, HEAP_NOTHEAP, val); } break; case NSS_SCHEME_DESC: @@ -1129,7 +1129,7 @@ cygheap_pwdgrp::get_shell (cyg_ldap *pldap, cygpsid &sid, PCWSTR dom, shell = (char *) cygwin_create_path (CCP_WIN_W_TO_POSIX, val); else - sys_wcstombs_alloc (&shell, HEAP_NOTHEAP, val); + sys_wcstombs_alloc_path (&shell, HEAP_NOTHEAP, val); } } break; From 12b2da4acd15ba908537173c0fe6191203ea153b Mon Sep 17 00:00:00 2001 From: Richard Glidden Date: Thu, 24 Aug 2023 13:36:10 -0400 Subject: [PATCH 080/102] msys2-runtime: restore fast path for current user primary group Commit a5bcfe616c7e removed an optimization that fetches the default group from the current user token, as it is sometimes not accurate such as when groups like the builtin Administrators group is the primary group. However, removing this optimization causes extremely poor performance when connected to some Active Directory environments. Restored this optimization as the default behaviour, and added a `group: db-accurate` option to `nsswitch.conf` that can be used to disable the optimization in cases where accurate group information is required. This fixes https://github.com/git-for-windows/git/issues/4459 Signed-off-by: Richard Glidden --- winsup/cygwin/include/sys/cygwin.h | 3 ++- winsup/cygwin/local_includes/cygheap.h | 1 + winsup/cygwin/uinfo.cc | 30 ++++++++++++++++++++------ winsup/doc/ntsec.xml | 20 ++++++++++++++++- 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/winsup/cygwin/include/sys/cygwin.h b/winsup/cygwin/include/sys/cygwin.h index 0e11a9b81a..5a99f758a7 100644 --- a/winsup/cygwin/include/sys/cygwin.h +++ b/winsup/cygwin/include/sys/cygwin.h @@ -219,7 +219,8 @@ enum enum { NSS_SRC_FILES = 1, - NSS_SRC_DB = 2 + NSS_SRC_DB = 2, + NSS_SRC_DB_ACCURATE = 4 }; /* Enumeration source constants for CW_SETENT called from mkpasswd/mkgroup. */ diff --git a/winsup/cygwin/local_includes/cygheap.h b/winsup/cygwin/local_includes/cygheap.h index d9e936c1e4..0b3fed297a 100644 --- a/winsup/cygwin/local_includes/cygheap.h +++ b/winsup/cygwin/local_includes/cygheap.h @@ -405,6 +405,7 @@ class cygheap_pwdgrp inline int nss_pwd_src () const { return pwd_src; } /* CW_GETNSS_PWD_SRC */ inline bool nss_grp_files () const { return !!(grp_src & NSS_SRC_FILES); } inline bool nss_grp_db () const { return !!(grp_src & NSS_SRC_DB); } + inline bool nss_grp_db_accurate () const { return !!(grp_src & NSS_SRC_DB_ACCURATE); } inline int nss_grp_src () const { return grp_src; } /* CW_GETNSS_GRP_SRC */ inline bool nss_cygserver_caching () const { return caching; } inline void nss_disable_cygserver_caching () { caching = false; } diff --git a/winsup/cygwin/uinfo.cc b/winsup/cygwin/uinfo.cc index 3d3b804c83..375a016124 100644 --- a/winsup/cygwin/uinfo.cc +++ b/winsup/cygwin/uinfo.cc @@ -643,6 +643,11 @@ cygheap_pwdgrp::nss_init_line (const char *line) *src |= NSS_SRC_DB; c += 2; } + else if (NSS_CMP ("db-accurate")) + { + *src |= NSS_SRC_DB | NSS_SRC_DB_ACCURATE; + c += 11; + } else { c += strcspn (c, " \t"); @@ -1958,6 +1963,7 @@ pwdgrp::fetch_account_from_windows (fetch_user_arg_t &arg, bool ugid_caching, cy gid_t gid = ILLEGAL_GID; bool is_domain_account = true; PCWSTR domain = NULL; + bool get_default_group_from_current_user_token = false; char *shell = NULL; char *home = NULL; char *gecos = NULL; @@ -2472,9 +2478,19 @@ pwdgrp::fetch_account_from_windows (fetch_user_arg_t &arg, bool ugid_caching, cy uid = posix_offset + sid_sub_auth_rid (sid); if (!is_group () && acc_type == SidTypeUser) { - /* Default primary group. Make the educated guess that the user - is in group "Domain Users" or "None". */ - gid = posix_offset + DOMAIN_GROUP_RID_USERS; + /* Default primary group. If the sid is the current user, and + we are not configured for accurate mode, fetch + the default group from the current user token, otherwise make + the educated guess that the user is in group "Domain Users" + or "None". */ + if (!cygheap->pg.nss_grp_db_accurate() && sid == cygheap->user.sid ()) + { + get_default_group_from_current_user_token = true; + gid = posix_offset + + sid_sub_auth_rid (cygheap->user.groups.pgsid); + } + else + gid = posix_offset + DOMAIN_GROUP_RID_USERS; } if (is_domain_account) @@ -2482,9 +2498,11 @@ pwdgrp::fetch_account_from_windows (fetch_user_arg_t &arg, bool ugid_caching, cy /* Skip this when creating group entries and for non-users. */ if (is_group() || acc_type != SidTypeUser) break; - /* Fetch primary group from AD and overwrite the one we - just guessed above. */ - if (cldap->fetch_ad_account (sid, false, domain)) + /* For the current user we got correctly cased username and + the primary group via process token. For any other user + we fetch it from AD and overwrite it. */ + if (!get_default_group_from_current_user_token + && cldap->fetch_ad_account (sid, false, domain)) { if ((val = cldap->get_account_name ())) wcscpy (name, val); diff --git a/winsup/doc/ntsec.xml b/winsup/doc/ntsec.xml index ae9270e6e3..768c75a5ec 100644 --- a/winsup/doc/ntsec.xml +++ b/winsup/doc/ntsec.xml @@ -930,7 +930,16 @@ The two lines starting with the keywords passwd: and information from. files means, fetch the information from the corresponding file in the /etc directory. db means, fetch the information from the Windows account databases, the SAM -for local accounts, Active Directory for domain account. Examples: +for local accounts, Active Directory for domain account. For the current +user, the default group is obtained from the current user token to avoid +additional lookups to the group database. db-accurate +is only valid on group: line, and performs the same +lookups as the db option, but disables using the +current user token to retrieve the default group as this optimization +is not accurate in all cases. For example, if you run a native process +with the primary group set to the Administrators builtin group, the +db option will return a non-existent group as primary +group. Examples: @@ -949,6 +958,15 @@ Read passwd entries only from /etc/passwd. Read group entries only from SAM/AD. + + group: db-accurate + + + +Read group entries only from SAM/AD. Force the use of the group database +for the current user. + + group: files # db From 370e5af99b0ce535a86fcc3fce130a7b2c8eb967 Mon Sep 17 00:00:00 2001 From: Mikael Larsson <95430516+chirpnot@users.noreply.github.com> Date: Thu, 10 Mar 2022 17:26:42 +0000 Subject: [PATCH 081/102] Change the default base address for x86_64 This might break things, but it turns out several Windows libraries like to be loaded at 0x180000000. This causes a problem, because `msys-2.0.dll` loads at `0x180040000` and expects `0x180000000-0x180040000` to be available. A problem arises when Antiviruses (or other DLL hooking mechanisms) load a DLL whose preferred load address is `0x180000000` and fits in size before `0x180010000`: 1. `msys-2.0.dll` loads and fills `0x180010000-0x180040000` assuming no shared console structure is going to be needed. 2. Another DLL loads and fills `0x180000000-0x18000xxxx` 3. `msys-2.0.dll` tries to load `0x180000000-0x180010000` but it's not available. It falls back to another address, but down the line something else fails. This bug triggers when using subshells (e.g.: `git clone --recursive`). The MSYS2 runtime should be able to work around the address conflict, but the code is failing in some way or other... Signed-off-by: Johannes Schindelin Signed-off-by: Mikael Larsson <95430516+chirpnot@users.noreply.github.com> --- winsup/cygwin/cygwin.din | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winsup/cygwin/cygwin.din b/winsup/cygwin/cygwin.din index 073b8d07f4..5f8ce5939e 100644 --- a/winsup/cygwin/cygwin.din +++ b/winsup/cygwin/cygwin.din @@ -1,4 +1,4 @@ -LIBRARY "msys-2.0.dll" BASE=0x180040000 +LIBRARY "msys-2.0.dll" BASE=0x210040000 EXPORTS # Exported variables From f45cc103948aa569b6cabf0af76f57a4fb82a101 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2026 12:17:50 +0100 Subject: [PATCH 082/102] Add AGENTS.md with comprehensive project context for AI agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This file documents the layered fork structure of this repository (Cygwin → MSYS2 → Git for Windows), the merging-rebase strategy that keeps the main branch fast-forwarding, the build system and its bootstrap chicken-and-egg nature (msys-2.0.dll is the POSIX emulation layer that its own GCC depends on), the CI pipeline, key directories and files, development guidelines, and external resources. The intent is to give AI coding agents enough context to work competently on this codebase without hallucinating about its structure or purpose. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- AGENTS.md | 341 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..e05270ae56 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,341 @@ +# Guidelines for AI Agents Working on This Codebase + +## Project Overview + +This repository is the **Git for Windows fork** of the **MSYS2 runtime**, which is itself a fork of the **Cygwin runtime**. The runtime provides a POSIX emulation layer on Windows, producing `msys-2.0.dll` (analogous to Cygwin's `cygwin1.dll`). It is the foundational component that allows Unix-style programs (bash, coreutils, etc.) to run on Windows within the MSYS2 and Git for Windows ecosystems. + +### The Layered Fork Structure + +There are three layers of this project, each building on the one below: + +1. **Cygwin** (`git://sourceware.org/git/newlib-cygwin.git`, releases at https://cygwin.com): The upstream project. Cygwin is a POSIX-compatible environment for Windows consisting of a DLL (`cygwin1.dll`) that provides substantial POSIX API functionality, plus a collection of GNU and Open Source tools. The Cygwin project releases versioned tags (e.g., `cygwin-3.6.6`) from the `cygwin/cygwin` GitHub mirror. + +2. **MSYS2** (`https://github.com/msys2/msys2-runtime`): The MSYS2 project rebases its own patches on top of each Cygwin release. MSYS2 maintains branches named `msys2-X.Y.Z` (e.g., `msys2-3.6.6`) where the Cygwin code is the base and MSYS2-specific patches are applied on top. These patches implement features like POSIX-to-Windows path conversion (`msys2_path_conv.cc`), the `MSYS` environment variable for controlling runtime behavior, pseudo-console support toggling, and adaptations needed for MSYS2's focus on building native Windows software (as opposed to Cygwin's focus on running Unix software on Windows as-is). + +3. **Git for Windows** (`https://github.com/git-for-windows/msys2-runtime`, this repository): Git for Windows maintains a "merging rebase" on top of the MSYS2 patches. The `main` branch uses a special strategy where it always fast-forwards. Each rebase to a new upstream version starts with a "fake merge" commit (message: `Start the merging-rebase to cygwin-X.Y.Z`) that merges previous `main` using the `-s ours` strategy. This ensures the branch always fast-forwards despite being rebased. Git for Windows' own patches (on top of MSYS2's patches) address issues specific to Git's usage patterns, such as Ctrl+C signal handling, SSH hang fixes, and console output correctness. + +### Key Relationships + +- **Cygwin → MSYS2**: MSYS2 rebases onto each Cygwin release. When Cygwin releases version X.Y.Z, an `msys2-X.Y.Z` branch is created with MSYS2 patches rebased on top. +- **MSYS2 → Git for Windows**: Git for Windows performs a merging rebase that first merges in the MSYS2 patches, then rebases its own patches on top. +- The `main` branch in this repository (git-for-windows/msys2-runtime) is the Git for Windows branch, not Cygwin's or MSYS2's. + +## Repository Structure + +### Key Directories + +- **`winsup/cygwin/`**: The core of the Cygwin/MSYS2 runtime. This is where `msys-2.0.dll` (the POSIX emulation DLL) is built. Most development work happens here. Key files include: + - `dcrt0.cc`: Runtime initialization + - `spawn.cc`: Process spawning + - `path.cc`: Path handling + - `fork.cc`: fork() implementation + - `exceptions.cc`: Signal handling + - `msys2_path_conv.cc` / `msys2_path_conv.h`: MSYS2-specific POSIX-to-Windows path conversion (CC0-licensed) + - `environ.cc`: Environment variable handling, including the `MSYS` environment variable + - `fhandler/`: File handler implementations for various device types + - `local_includes/`: Internal headers + - `release/`: Version history files (one per Cygwin release version) +- **`winsup/utils/`**: Cygwin/MSYS2 utility programs (mount, cygpath, etc.) +- **`newlib/`**: The C library (newlib) used by the runtime +- **`ui-tests/`**: AutoHotKey-based integration tests that test the runtime in real terminal scenarios +- **`.github/workflows/`**: CI configuration + +## Build System + +### The Chicken-and-Egg Problem + +The MSYS2 runtime (`msys-2.0.dll`) is itself the POSIX emulation layer that the MSYS2 toolchain (GCC, binutils, etc.) depends on. The MSYS2 environment's own GCC links against `msys-2.0.dll` to provide POSIX semantics. This means you need a working MSYS2 runtime to compile a new MSYS2 runtime — a classic bootstrap problem. + +In practice, this is resolved by using an existing MSYS2 installation to build the new version. The CI workflow (`.github/workflows/build.yaml`) installs MSYS2 via the `msys2/setup-msys2` action, then builds the new runtime within that environment. + +### Build Dependencies + +Building requires MSYS2 packages: `msys2-devel`, `base-devel`, `autotools`, `cocom`, `gcc`, `gettext-devel`, `libiconv-devel`, `make`, `mingw-w64-cross-crt`, `mingw-w64-cross-gcc`, `mingw-w64-cross-zlib`, `perl`, `zlib-devel`. These are all **msys** packages (they link against `msys-2.0.dll`), not native MinGW packages. + +### Building in the Git for Windows SDK + +The Git for Windows SDK provides a complete MSYS2 environment with all necessary build dependencies pre-installed. The source tree is typically located at `/usr/src/MSYS2-packages/msys2-runtime/src/msys2-runtime` inside the SDK. + +**Critical: PATH ordering.** The build must use the MSYS2 toolchain, not any MinGW toolchain that might be on the PATH. Before building, ensure: + +```bash +export PATH=/usr/bin:/mingw64/bin:/mingw32/bin:$PATH +``` + +If MinGW's GCC is found first, the build will fail because MinGW tools do not link against `msys-2.0.dll` and cannot produce the runtime DLL. + +### Build Commands + +```bash +# Generate autotools files +(cd winsup && ./autogen.sh) + +# Configure (the --with-msys2-runtime-commit flag embeds the commit hash) +./configure --disable-dependency-tracking --with-msys2-runtime-commit="$(git rev-parse HEAD)" + +# Build +make -j8 +``` + +For quick rebuilds of just the DLL during development: +```bash +# Rebuild only msys-2.0.dll +make -C ../build-x86_64-pc-msys/x*/winsup/cygwin -j15 new-msys-2.0.dll +``` + +The build output is `new-msys-2.0.dll` in the build directory. This is a staging name to avoid overwriting the running DLL. + +### Testing a Locally-Built DLL + +You cannot replace the SDK's own `msys-2.0.dll` while running inside the SDK — the DLL is loaded by every MSYS2 process including your shell. Instead, copy the built DLL into a separate installation such as a Portable Git: + +```bash +cp new-msys-2.0.dll /path/to/PortableGit/usr/bin/msys-2.0.dll +``` + +Then run tests using that Portable Git's mintty/bash. Back up the original DLL first. + +The `build-and-copy.sh` helper script in the repository root can reconfigure, rebuild, and copy `msys-2.0.dll` to a target location. + +### Internal API Constraints + +Code inside `msys-2.0.dll` cannot use the full C runtime or C++ standard library freely. Key limitations: + +- **`__small_sprintf`** is used instead of `sprintf`. It does NOT support `%lld` (64-bit integers) or floating-point format specifiers. For 64-bit values, split into high/low 32-bit halves and print as two `%u` values. +- **Memory allocation** in low-level code (e.g., DLL initialization, atexit handlers) should use `HeapAlloc(GetProcessHeap(), ...)` to avoid circular dependencies with the Cygwin malloc. + +### CI Pipeline + +The CI (`.github/workflows/build.yaml`) does the following: +1. **Build**: Compiles the runtime on `windows-latest` using MSYS2 +2. **Minimal SDK artifact**: Creates a minimal Git for Windows SDK with the just-built runtime, used for testing Git itself +3. **Test minimal SDK**: Runs Git's test suite against the new runtime +4. **UI tests**: AutoHotKey-based integration tests for terminal behavior (Ctrl+C interrupts, SSH operations, etc.) +5. **MSYS2 tests**: Runs the MSYS2 project's own test suite across multiple environments and compilers + +## Git Branch and Rebase Workflow + +### The Merging Rebase Strategy + +Git for Windows uses a "merging rebase" to maintain a fast-forwarding `main` branch. The key insight is a "fake merge" commit that: + +1. Starts from the new upstream commit (Cygwin tag) +2. Merges in the previous `main` using `-s ours` (takes NO changes from previous main, only the tree from upstream) +3. This makes `main` a parent of the new commit, so the result is a fast-forward from previous `main` +4. Patches are then rebased on top of this fake merge + +The commit message follows a strict format: `Start the merging-rebase to cygwin-X.Y.Z`. This is machine-parseable — `git rev-parse 'main^{/^Start.the.merging-rebase}'` finds the most recent such commit. + +### History of Merging Rebases + +The repository has been continuously rebased through Cygwin versions from 3.3.x through the current 3.6.6. Each rebase is visible as a `Start the merging-rebase to cygwin-X.Y.Z` commit on `main`. + +### Key Branches + +- `main`: Git for Windows' branch (fast-forwarding, contains merging-rebase commits) +- `cygwin-X_Y-branch` (e.g., `cygwin-3_6-branch`): Tracking branches for upstream Cygwin +- `cygwin/main`: Upstream Cygwin's main branch +- Various feature branches for specific fixes (e.g., `fix-ctrl+c-again`, `fix-ssh-hangs-reloaded`) + +### Key Remotes + +- `cygwin`: The upstream Cygwin repository (`git://sourceware.org/git/newlib-cygwin.git`) +- `msys2`: The MSYS2 fork (`https://github.com/msys2/msys2-runtime`) +- `git-for-windows`: This repository (`https://github.com/git-for-windows/msys2-runtime`) +- `dscho`: Johannes Schindelin's fork (primary maintainer) + +## Development Guidelines + +### Language and Style + +The runtime is written in **C++** (with some C). The code uses Cygwin's existing coding conventions. When modifying files under `winsup/cygwin/`: +- Follow the existing indentation and brace style of each file +- Cygwin code uses 8-space tabs in many files +- MSYS2-specific additions (like `msys2_path_conv.cc`) may use different conventions + +### Making Changes + +Most changes for Git for Windows purposes are in `winsup/cygwin/`. Common areas of modification: +- Signal handling (`exceptions.cc`, `sigproc.cc`) +- Process spawning (`spawn.cc`) +- PTY/console handling (`fhandler/` directory, `termios.cc`) +- Path conversion (`msys2_path_conv.cc`, `path.cc`) +- Environment handling (`environ.cc`) + +### Testing + +- The CI builds the runtime and runs Git's entire test suite against it +- UI tests in `ui-tests/` test real terminal scenarios using AutoHotKey +- MSYS2's own test suite is run across multiple compiler/environment combinations +- For local testing, build the DLL and copy it to replace `msys-2.0.dll` in an MSYS2 installation + +### Commit Discipline + +- One logical change per commit +- Commit messages should explain context, intent, and justification in prose (not bullet points) +- For the rebase workflow, commit messages follow specific patterns (e.g., `Start the merging-rebase to ...`) that tooling depends on — do not alter these patterns + +### Cygwin Commit Message Format + +Commits that modify code under `winsup/cygwin/` should follow the Cygwin project's commit message conventions, as established by the upstream maintainers (Corinna Vinschen, Takashi Yano, et al.): + +- **Subject prefix**: `Cygwin: : `, where `` is the subsystem (e.g. `pty`, `flock`, `termios`, `uinfo`, `path`, `spawn`). Example: `Cygwin: pty: Fix jumbled keystrokes by removing the per-keystroke pipe transfer`. Both upper-case and lower-case after the prefix are used upstream; there is no strict rule. +- **`Fixes:` trailer**: When a commit fixes a bug introduced by a specific earlier commit, reference it with `Fixes: <12-char-hash> ("")`. Example: `Fixes: acc44e09d1d0 ("Cygwin: pty: Add missing input transfer when switch_to_pcon_in state.")` +- **`Addresses:` trailer**: Reference the user-visible bug report URL. Example: `Addresses: https://github.com/git-for-windows/git/issues/5632` +- **Trailer ordering**: `Addresses:`, then `Fixes:`, then `Assisted-by:` / `Reviewed-by:` / `Reported-by:`, then `Signed-off-by:` last — following the pattern seen in upstream Cygwin commits. + +## PTY Architecture — Pipes, State Machine, and Input Routing + +This section documents the internal architecture of the pseudo-terminal (PTY) implementation in `winsup/cygwin/fhandler/pty.cc`. Understanding this is essential for debugging any issue involving terminal input/output, keystroke handling, signal delivery, and process foreground/background transitions. + +### Background: Why This Matters + +The pseudo console support in the Cygwin runtime is one of the most intricate subsystems in this codebase. It bridges two fundamentally different models of terminal I/O — POSIX and Win32 console — across multiple processes that share state through shared memory. The implementation is ambitious and evolving; the complexity of the interactions between pipe switching, pseudo console lifecycle, cross-process mutexes, and foreground process detection means that changes in one area can have subtle, hard-to-diagnose effects elsewhere. Historically, bug fixes in this area have occasionally introduced new regressions, which is simply a reflection of how difficult the problem space is. Any AI agent working on PTY-related issues should take the time to understand the full picture before proposing changes, and should be especially careful about mutex acquisition order, state transitions that span process boundaries, and the distinction between the two pipe pairs described below. + +### The Two Pipe Pairs + +Each PTY has **two independent pipe pairs** for input, serving different consumers: + +1. **Cygwin (cyg) pipe**: `to_slave` / `from_master` + - Used when a **Cygwin/MSYS2 process** (e.g., bash) is in the foreground. + - Input goes through `line_edit()` (in `termios.cc`) which handles line discipline (echo, canonical mode, special characters) before being written via `accept_input()`. + - The slave reads from `from_master` (aliased as `get_handle()` on the slave side). + +2. **Native (nat) pipe**: `to_slave_nat` / `from_master_nat` + - Used when a **non-Cygwin (native Windows) process** (e.g., `powershell.exe`, `cmd.exe`, a MinGW program) is in the foreground. + - When the pseudo console (pcon) is active, `CreatePseudoConsole()` wraps this pipe pair. The Windows `conhost.exe` process reads from `from_master_nat` and provides console input semantics to the native app. + - The master writes directly to `to_slave_nat` via `WriteFile()`, bypassing `line_edit()`. + +For **output**, there is a corresponding pair (`to_master` / `to_master_nat`) plus a forwarding thread (`master_fwd_thread`) that copies output from the nat pipe's slave side (`from_slave_nat`) to the cyg pipe's master side (`to_master`), so the terminal emulator (mintty) always reads from one place. + +### The Designed Keystroke Lifecycle + +Understanding the full lifecycle of a keystroke is essential. The design intent is that **no keystroke is ever lost**, regardless of what the foreground process does with it. The lifecycle differs between Cygwin and native foreground processes: + +**When a Cygwin process is in the foreground (e.g., bash):** +1. Terminal emulator writes keystroke via `master::write()` +2. `master::write()` calls `line_edit()` which applies POSIX line discipline +3. `accept_input()` writes processed bytes to the cyg pipe +4. The Cygwin slave (bash) reads from the cyg pipe + +**When a native process is in the foreground with pcon active:** +1. Terminal emulator writes keystroke via `master::write()` +2. `master::write()` fast path writes directly to `to_slave_nat` (nat pipe) +3. Conhost (the pseudo console host) reads from the nat pipe, converts the byte stream to `INPUT_RECORD` events, and stores them in its console input buffer +4. If the native process reads stdin: it gets `INPUT_RECORD` events via `ReadConsoleInput()` +5. If the native process does NOT read stdin (common for background tasks): the `INPUT_RECORD` events accumulate in conhost's buffer +6. When the native process exits: `cleanup_for_non_cygwin_app()` calls `transfer_input(to_cyg)`, which reads all pending `INPUT_RECORD` events from the console buffer via `ReadConsoleInputA()`, converts them back to bytes, and writes them to the cyg pipe +7. Bash's readline then receives these bytes as if they had been typed directly + +**Step 6 is critical and easy to overlook.** Keystrokes that go to the nat pipe during a native process's lifetime are NOT consumed by the native app (unless it explicitly reads them). They accumulate in conhost's input buffer and are transferred back to bash at cleanup. The transfer happens via `ReadConsoleInputA()` (raw event reads, not cooked/line-edited), so backspaces, escape sequences, and control characters are preserved as-is. + +**Consequence for debugging:** If keystrokes appear reordered at bash's readline after a native process exits, the problem is that some bytes went to the cyg pipe (directly to readline) while others went to the nat pipe (and were transferred back later). The bytes that went directly arrive first; the transferred bytes arrive second. This split delivery causes reordering. The fix must ensure that ALL keystrokes go through the same pipe during the native process's lifetime. + +### The Pseudo Console (pcon) + +When `MSYS=disable_pcon` is NOT set (the default), the runtime uses Windows' `CreatePseudoConsole()` API to give native console applications a real console to talk to. The pseudo console is created on demand when a non-Cygwin process becomes the foreground process, and torn down when it exits. This is what allows programs like `cmd.exe`, `powershell.exe`, or any MinGW-built program to work correctly inside a mintty terminal, which has no native Win32 console of its own. + +The pcon lifecycle is managed across process boundaries: the slave process (running the non-Cygwin app) and the master process (the terminal emulator) both participate. This cross-process coordination is the source of much of the complexity. + +Key state fields in the `tty` structure (shared memory, in `tty.h`): + +- **`pcon_activated`** (`bool`): True when a pseudo console is currently active. +- **`pcon_start`** (`bool`): True during pseudo console initialization (CSI6n exchange). +- **`pcon_start_pid`** (`pid_t`): PID of the process that initiated pcon setup. + +### The Input State Machine + +The field **`pty_input_state`** (type `xfer_dir`, in `tty.h:137`) tracks which pipe pair currently "owns" the input. It has two values: + +- **`to_cyg`**: Input is flowing to the Cygwin pipe. The master's `write()` uses the `line_edit()` -> `accept_input()` path, which writes to `to_slave` (cyg pipe). +- **`to_nat`**: Input is flowing to the native pipe. The master's `write()` writes directly to `to_slave_nat` (nat pipe), or through the pseudo console. + +The state transitions happen via **`transfer_input()`**, which: +1. Reads all pending data from the "source" pipe (the one being abandoned). +2. Writes that data into the "destination" pipe (the one being switched to). +3. Sets `pty_input_state` to the new direction. + +This ensures data already buffered in one pipe is not lost when switching. + +**When transferred input goes to the cyg pipe (to_cyg direction),** it must pass through `line_edit()` to apply POSIX line discipline. This is handled by the `input_transferred_to_cyg` event: the slave signals this event after the transfer, and the master's forward thread wakes up, reads the transferred bytes from the cyg pipe, and processes them through `line_edit()`. This ensures consistent line discipline regardless of whether input arrived via direct typing or via transfer. + +### Related State Fields + +- **`switch_to_nat_pipe`** (`bool`): Set to true when a non-Cygwin process is detected in the foreground. This is a prerequisite for `to_be_read_from_nat_pipe()` returning true. It stays true for the entire duration of the native session, including during brief transitions when `pcon_activated` may flicker. +- **`nat_pipe_owner_pid`** (`DWORD`): PID of the process that "owns" the nat pipe setup. Used to detect when the owner has exited (for cleanup). + +### The `to_be_read_from_nat_pipe()` Function + +This function determines whether keystroke input should go to the nat pipe. Its design intent is simple: return true whenever a native process session is active (`switch_to_nat_pipe` is set) and no Cygwin process is actively reading from the slave (the `TTY_SLAVE_READING` event does not exist). + +The function is synchronized with `pipe_sw_mutex` to avoid reading inconsistent state during pipe switching. If the mutex cannot be acquired and `pcon_start` is set (meaning pseudo console initialization is in progress), the function returns false so that the CSI6n response bytes go through `line_edit()` to the cyg pipe where the initialization code expects them. + +**Important design principle:** This function should NOT check `nat_fg()` (whether the native process is still in the foreground process group). Such a check creates a gap between native process exit and cleanup where keystrokes fall through to `line_edit()` (cyg pipe) instead of going to the nat pipe. This gap causes keystroke reordering: bytes that go directly to the cyg pipe during the gap arrive at readline before bytes that are transferred from the nat pipe at cleanup. The correct approach is to keep routing to the nat pipe as long as `switch_to_nat_pipe` is set, regardless of the native process's foreground status. The `switch_to_nat_pipe` flag is only cleared during cleanup, after `transfer_input(to_cyg)` has moved all pending data back to the cyg pipe. + +### Mutexes and Synchronization + +Three cross-process named mutexes protect different aspects of the PTY state: + +- **`input_mutex`**: Protects the input data path. Held by `master::write()` while routing input to a pipe, by `transfer_input()` while moving data between pipes, and by `line_edit()` / `accept_input()`. +- **`pipe_sw_mutex`**: Protects pipe switching state — creation/destruction of the pseudo console, changes to `switch_to_nat_pipe`, `nat_pipe_owner_pid`. Also acquired by `to_be_read_from_nat_pipe()` to read consistent state. The consistent lock ordering is: `pipe_sw_mutex` first, then `input_mutex`. +- **`attach_mutex`**: Protects console attachment/detachment operations. Used during `transfer_input()` to prevent races when reading console input records via `ReadConsoleInputA()`, and in `get_winpid_to_hand_over()` to prevent the master process from being misidentified during temporary console attachment. + +Because these are **cross-process** named mutexes, they are shared via the kernel between the master (terminal emulator) and slave (bash and its children) processes. Operations that look local in the source code actually have system-wide synchronization effects. + +### The `master::write()` Input Routing + +When the terminal emulator (mintty) sends a keystroke, it calls `master::write()`. The function has three code paths: + +1. **pcon_start handler**: Active during pseudo console initialization (CSI6n exchange). Accumulates ESC sequence bytes and routes the CSI6n response to the slave. Non-response bytes go through `line_edit()`. This path is only active during the brief initialization window. + +2. **Fast path** (pcon+nat): Active when `to_be_read_from_nat_pipe()` AND `pcon_activated` AND `pty_input_state == to_nat`. Writes directly to `to_slave_nat` via `WriteFile()`, with signal processing and charset conversion. This is the steady-state path for native apps. + +3. **Fallthrough** (`line_edit`): All other cases. Input goes through POSIX line discipline and `accept_input()` routes to the appropriate pipe based on `pty_input_state`. + +### Pseudo Console Oscillation + +When a native process spawns short-lived Cygwin children (e.g. `git.exe` calling `cygpath` via `--format`), the pseudo console activates and deactivates in rapid succession: + +1. Native process in foreground: `pcon_activated=true`, `pty_input_state=to_nat` +2. Cygwin child starts: `setpgid_aux()` fires, transfers data to cyg pipe, `pcon_activated=false`, `pty_input_state=to_cyg` +3. Cygwin child exits (milliseconds later): native process regains foreground, pcon reactivates + +**The key design principle for handling oscillation:** keystrokes must always go to the nat pipe while `switch_to_nat_pipe` is true, regardless of `pcon_activated` or foreground status flickering. When keystrokes reach the nat pipe while the pcon is temporarily deactivated, they go through the raw pipe (not via conhost). When `transfer_input` runs at cleanup, it moves them back. This is safe because the keystrokes stay in the nat pipe in chronological order. + +The bugs that cause keystroke reordering are always of the form: some bytes go to the cyg pipe (via `line_edit` fallthrough) while others go to the nat pipe (via the fast path), and the two sets arrive at bash's readline in the wrong order. The fix is to prevent the split: either ALL bytes go to one pipe, or the routing decision is properly synchronized so that no bytes leak to the wrong pipe. + +### Key Functions for State Transitions + +- **`setup_for_non_cygwin_app()`**: Called when a non-Cygwin process is about to be spawned. Sets up the pseudo console and switches input to nat pipe. Holds `pipe_sw_mutex` during the entire setup to prevent the master from seeing inconsistent state. +- **`cleanup_for_non_cygwin_app()`**: Called when the non-Cygwin process exits. First calls `transfer_input(to_cyg)` to move all pending input from the nat pipe (conhost's console buffer) back to the cyg pipe. Then tears down the pcon via `close_pseudoconsole()`. The transfer must happen BEFORE the pcon is closed (while the console is still accessible). +- **`reset_switch_to_nat_pipe()`**: Cleanup function called from `bg_check()` and `setpgid_aux()`. Detects when the nat pipe owner has exited and resets state. Only performs cleanup when no other process owns the nat pipe and the owner is dead. Does NOT clean up when the owner is self (bash) or alive, to avoid tearing down active sessions. +- **`transfer_input()`**: Moves pending data between the cyg and nat pipes. When transferring to cyg with pcon active, reads `INPUT_RECORD` events from the console via `ReadConsoleInputA()`. When transferring to cyg, signals `input_transferred_to_cyg` so the master's forward thread can apply `line_edit()` to the transferred bytes. +- **`setpgid_aux()`**: Called when the foreground process group changes. Triggers `transfer_input` in the appropriate direction. Releases `pipe_sw_mutex` before acquiring `input_mutex` to maintain consistent lock ordering. + +### Debugging Tips + +When investigating PTY-related bugs, keep these patterns in mind: + +- **Trace the full keystroke lifecycle**: Do not stop at "the keystroke goes to pipe X." Follow it all the way to where bash's readline receives it, including any `transfer_input` calls at cleanup. The most common bugs involve bytes being split across the two pipes and arriving at readline out of order. +- **Check the routing decision in `to_be_read_from_nat_pipe()`**: This function is the gatekeeper for all routing decisions. If it returns the wrong value, keystrokes go to the wrong pipe. Verify that it holds `pipe_sw_mutex` while reading state, and that it does not have unnecessary checks (like `nat_fg()`) that create gaps during transitions. +- **Study existing upstream patches before writing fixes**: Takashi Yano is the upstream PTY maintainer and understands the state machine deeply. When he proposes patches on cygwin-patches@, apply and test his full series before attempting alternative fixes. His patches form cohesive sets where individual patches depend on each other for correct behavior. Cherry-picking individual patches from his series will break invariants. +- **Never remove `transfer_input()` calls without understanding what they transfer**: The transfers at `setpgid_aux()`, `cleanup_for_non_cygwin_app()`, and the pcon_start completion block each serve specific purposes. Removing them loses data. The correct fix for reordering bugs is to ensure keystrokes consistently go to one pipe (typically by fixing the routing decision), not to remove the transfer that reunites split data. +- **The `pcon_start` handler is only for CSI6n**: During pcon initialization, `pcon_start=true` tells `master::write()` to enter a special handler that accumulates the CSI6n response. Non-CSI bytes in this handler go through `line_edit()` to the cyg pipe. This is correct and intentional: during the brief CSI6n exchange, the pcon is not yet ready to receive user input, so `line_edit()` buffers it for bash. The pcon_start handler is NOT a general-purpose input router and should not be modified to route bytes to the nat pipe. +- **Tracing**: For timing-sensitive bugs, use a memory-mapped ring buffer (not per-event file I/O, which changes timings). The master process (mintty) is a MinGW program; C++ static destructors in msys-2.0.dll do NOT fire when it exits. Use `CreateFileMapping` + `MapViewOfFile` for trace buffers that persist after process termination. Use `QueryPerformanceCounter` for microsecond timestamps. Trace across both master and slave processes using separate per-PID files. + +## Packaging + +The MSYS2 runtime is packaged as an **msys** package (`msys2-runtime`) using `makepkg` with a `PKGBUILD` recipe in the `msys2/MSYS2-packages` repository. The package definition lives at `msys2-runtime/PKGBUILD` in that repository. + +## External Resources + +- **Cygwin project**: https://cygwin.com — upstream source, FAQ, user's guide +- **Cygwin source**: https://github.com/cygwin/cygwin (mirror of `sourceware.org/git/newlib-cygwin.git`) +- **Cygwin announcements**: https://inbox.sourceware.org/cygwin-announce — release announcements +- **Cygwin mailing lists**: https://inbox.sourceware.org/cygwin/ (general), https://inbox.sourceware.org/cygwin-patches/ (patches), https://inbox.sourceware.org/cygwin-developers/ (internals) — essential for understanding why specific code was added; commit messages often reference these discussions +- **MSYS2 project**: https://www.msys2.org — documentation, package management +- **MSYS2 runtime source**: https://github.com/msys2/msys2-runtime +- **MSYS2 packages**: https://github.com/msys2/MSYS2-packages — package recipes including `msys2-runtime` +- **Git for Windows**: https://gitforwindows.org +- **Git for Windows runtime**: https://github.com/git-for-windows/msys2-runtime (this repository) +- **MSYS2 environments**: https://www.msys2.org/docs/environments/ — explains MSYS vs UCRT64 vs CLANG64 etc. From adf0787e6d08356227b0f56050711472036708f5 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 21 Apr 2026 19:43:28 +0200 Subject: [PATCH 083/102] Start implementing UI-based tests by adding an AutoHotKey library AutoHotKey is not only a convenient way to add keyboard shortcuts for functionality (or applications) that does not come with shortcuts, but it is in general a powerful language to remote control GUI elements. We will use this language to implement a couple of automated tests that should hopefully prevent regressions as we have experienced in the past (for example, a regression that was fixed and immediately re-broken, which went unnoticed for months). So let's start by adding a library of useful functions, to be extended as needed. Note: As AutoHotKey is a GUI application, it does not expect to have stdout/stderr attached to it, therefore the `Info()` function added in this commit writes all the messages into `.log` files adjacent to the per-test working directories. But AutoHotKey _can_ have stdout/stderr attached to it, via redirection. In PowerShell, for example, appending `| Out-Default` to the invocation will make stdout/stderr available to AutoHotKey scripts (via the unintuitive syntax `FileAppend "text`n", "*"` (and `"**"` for stderr). The `Info()` function will detect when stdout is available and if it is, will also write to it, in addition to the `.log` file. Signed-off-by: Johannes Schindelin --- ui-tests/ui-test-library.ahk | 119 +++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 ui-tests/ui-test-library.ahk diff --git a/ui-tests/ui-test-library.ahk b/ui-tests/ui-test-library.ahk new file mode 100644 index 0000000000..e6d261ed45 --- /dev/null +++ b/ui-tests/ui-test-library.ahk @@ -0,0 +1,119 @@ +; Reusable library functions for the UI tests. + +SetWorkTree(defaultName) { + global workTree + ; First, set the worktree path; This path will be reused + ; for the `.log` file). + if A_Args.Length > 0 + workTree := A_Args[1] + else + { + ; Create a unique worktree path in the TEMP directory. + workTree := EnvGet('TEMP') . '\' . defaultName + if FileExist(workTree) + { + counter := 0 + while FileExist(workTree '-' counter) + counter++ + workTree := workTree '-' counter + } + } + + SetWorkingDir(EnvGet('TEMP')) + Info 'uname: ' RunWaitOne('git -c alias.uname="!uname" uname -a') + Info RunWaitOne('git version --build-options') + + RunWait('git init "' workTree '"', '', 'Hide') + if A_LastError + ExitWithError 'Could not initialize Git worktree at: ' workTree + + SetWorkingDir(workTree) + if A_LastError + ExitWithError 'Could not set working directory to: ' workTree +} + +CleanUpWorkTree() { + global workTree + SetWorkingDir(EnvGet('TEMP')) + Info 'Cleaning up worktree: ' workTree + DirDelete(workTree, true) +} + +Info(text) { + global workTree, cannotWriteToStdout + FileAppend text '`n', workTree '.log' + if !IsSet(cannotWriteToStdout) + { + try + FileAppend text '`n', '*' + catch as e { + if e.__Class == 'OSError' && e.Number == 6 + cannotWriteToStdout:= false + else + throw e + } + } +} + +closeWindow := false +childPid := 0 +ExitWithError(error) { + Info 'Error: ' error + if closeWindow + WinClose "A" + else if childPid != 0 + ProcessClose childPid + ExitApp 1 +} + +RunWaitOne(command) { + SavedClipboard := ClipboardAll + shell := ComObject("WScript.Shell") + ; Execute a single command via cmd.exe + exec := shell.Run(A_ComSpec " /C " command " | clip", 0, true) + if exec != 0 + ExitWithError 'Error executing command: ' command + ; Read and return the command's output, trimming trailing newlines. + Result := RegExReplace(A_Clipboard, '`r?`n$', '') + Clipboard := SavedClipboard + return Result +} + +; Capture the Windows Terminal buffer via the exportBuffer action (Ctrl+Shift+F12). +; Requires a portable WT with settings.json that maps Ctrl+Shift+F12 to exportBuffer +; writing to /wt-buffer-export.txt. +CaptureBufferFromWindowsTerminal(winTitle := '') { + static exportFile := A_ScriptDir . '\wt-buffer-export.txt' + if FileExist(exportFile) + FileDelete exportFile + if winTitle != '' + WinActivate winTitle + Sleep 200 + Send '^+{F12}' + deadline := A_TickCount + 3000 + while !FileExist(exportFile) && A_TickCount < deadline + Sleep 50 + if !FileExist(exportFile) + return '' + Sleep 100 + return FileRead(exportFile) +} + +WaitForRegExInWindowsTerminal(regex, errorMessage, successMessage, timeout := 5000, winTitle := '') { + timeout := timeout + A_TickCount + ; Wait for the regex to match in the terminal output + while true + { + capturedText := CaptureBufferFromWindowsTerminal(winTitle) + if RegExMatch(capturedText, regex, &matchObj) + { + Info(successMessage) + return matchObj + } + Sleep 100 + if A_TickCount > timeout { + Info('Captured text:`n' . capturedText) + ExitWithError errorMessage + } + } +} \ No newline at end of file From 290e369e8d2a12e5702e4a889457c498c7242a9d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 7 May 2025 11:27:42 +0200 Subject: [PATCH 084/102] ci: add an AutoHotKey-based integration test The issue reported in https://github.com/microsoft/git/issues/730 was fixed, but due to missing tests for the issue a regression slipped in within mere weeks. Let's add an integration test that will (hopefully) prevent this issue from regressing again. This integration test is implement as an AutoHotKey script. It might look unnatural to use a script language designed to implement global keyboard shortcuts, but it is a quite powerful approach. While there are miles between the ease of developing AutoHotKey scripts and developing, say, Playwright tests, there is a decent integration into VS Code (including single-step debugging), and AutoHotKey's own development and community are quite vibrant and friendly. I had looked at alternatives to AutoHotKey, such as WinAppDriver, SikuliX, nut.js and AutoIt, in particular searching for a solution that would have a powerful recording feature similar to Playwright, but did not find any that is 1) mature, 2) well-maintained, 3) open source and 4) would be easy to integrate into a GitHub workflow. In the end, AutoHotKey appeared my clearest preference. So how is the test implemented? It lives in `ui-test/` and requires AutoHotKey v2 as well as Windows Terminal (the Legacy Prompt would not reproduce the problem). It then follows the reproducer I gave to the Cygwin team: 1. initialize a Git repository 2. install a `pre-commit` hook 3. this hook shall spawn a non-Cygwin/MSYS2 process in the background 4. that background process shall print to the console after Git exits 5. open a Command Prompt in Windows Terminal 6. run `git commit` 7. wait until the background process is done printing 8. press the Cursor Up key 9. observe that the Command Prompt does not react (in the test, it _does_ expect a reaction: the previous command in the command history should be shown, i.e. `git commit`) In my reproducer, I then also suggested to press the Enter key and to observe that now the "More ?" prompt is shown, but no input is accepted, until Ctrl+Z is pressed. Naturally, the test should not expect _that_ ;-) There were a couple of complications I needed to face when developing this test: - I did not find any easy macro recorder for AutoHotKey that I liked. It would not have helped much, anyway, because intentions are hard to record. - Before I realized that there is excellent AutoHotKey support in VS Code via the AutoHotKey++ and AutoHotKey Debug extensions, I struggled quite a bit to get the syntax right. - Windows Terminal does not use classical Win32 controls that AutoHotKey knows well. To capture the terminal text, we use Windows Terminal's exportBuffer action, which writes the entire scrollback to a file on a keybinding (Ctrl+Shift+F12). This requires running Windows Terminal in portable mode with a settings.json that defines the action, which the setup script takes care of. - Despite my expectations, `ExitApp` would not actually exit AutoHotKey before the spawned process exits and/or the associated window is closed. For good measure, run this test both on windows-2022 (corresponding to Windows 10) and on windows-2025 (corresponding to Windows 11). Note that this does not use the naive method to capture text from Windows Terminal, emulating mouse movements, dragging across the entire window with finicky pixel calculations for title bar height, scroll bar width and padding, then right-clicks to copy. This would be fragile: if the window geometry changes, if another window gets focus, or if the title bar height differs between OS versions, the capture silently gets the wrong text. Windows Terminal's exportBuffer action avoids all of that by writing the complete scrollback buffer to a file on a keybinding, with no dependence on pixel positions or window focus. To use it, Windows Terminal must run in portable mode with a settings.json that defines the action and keybinding. So that's what we do here. We add `setup-portable-wt.ps1`, which downloads Windows Terminal (when not already present), creates the .portable marker and writes settings.json with Ctrl+Shift+F12 bound to `exportBuffer`. It accepts a `-DestDir` parameter so CI can use `$RUNNER_TEMP` while local development uses `$TEMP`. When running inside GitHub Actions it also appends the Windows Terminal directory to `$GITHUB_PATH`. Co-authored-by: Eu-Pin Tien Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- .github/workflows/build.yaml | 8 +++ .github/workflows/ui-tests.yml | 87 ++++++++++++++++++++++++++++++ ui-tests/.gitattributes | 2 + ui-tests/background-hook.ahk | 54 +++++++++++++++++++ ui-tests/setup-portable-wt.ps1 | 96 ++++++++++++++++++++++++++++++++++ 5 files changed, 247 insertions(+) create mode 100644 .github/workflows/ui-tests.yml create mode 100644 ui-tests/.gitattributes create mode 100755 ui-tests/background-hook.ahk create mode 100644 ui-tests/setup-portable-wt.ps1 diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index af3fab83bf..558dbec973 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -119,6 +119,14 @@ jobs: with: git-artifacts-extract-location: ${{ needs.minimal-sdk-artifact.outputs.git-artifacts-extract-location }} + ui-tests: + needs: build + uses: ./.github/workflows/ui-tests.yml + with: + msys2-runtime-artifact-name: install + permissions: + contents: read + generate-msys2-tests-matrix: runs-on: ubuntu-latest outputs: diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml new file mode 100644 index 0000000000..d4f0ffd52b --- /dev/null +++ b/.github/workflows/ui-tests.yml @@ -0,0 +1,87 @@ +name: ui-tests + +on: + workflow_call: + inputs: + msys2-runtime-artifact-name: + required: true + type: string + +env: + AUTOHOTKEY_VERSION: 2.0.19 + WT_VERSION: 1.22.11141.0 + +jobs: + ui-tests: + strategy: + fail-fast: false + matrix: + # Corresponds to Windows Server versions + # See https://github.com/actions/runner-images?tab=readme-ov-file#available-images + os: [windows-2022, windows-2025] + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/download-artifact@v8 + with: + name: ${{ inputs.msys2-runtime-artifact-name }} + path: ${{ runner.temp }}/artifacts + - name: replace MSYS2 runtime + run: | + $p = Get-ChildItem -Recurse "${env:RUNNER_TEMP}\artifacts" | where {$_.Name -eq "msys-2.0.dll"} | Select -ExpandProperty VersionInfo | Select -First 1 -ExpandProperty FileName + cp $p "c:/Program Files/Git/usr/bin/msys-2.0.dll" + + - uses: actions/checkout@v7 + with: + sparse-checkout: | + ui-tests + + - uses: actions/cache/restore@v6 + id: restore-wt + with: + key: wt-${{ env.WT_VERSION }} + path: ${{ runner.temp }}/wt.zip + - name: Install and configure portable Windows Terminal + working-directory: ui-tests + run: | + powershell -File setup-portable-wt.ps1 -WtVersion $env:WT_VERSION -DestDir $env:RUNNER_TEMP + - uses: actions/cache/save@v6 + if: steps.restore-wt.outputs.cache-hit != 'true' + with: + key: wt-${{ env.WT_VERSION }} + path: ${{ runner.temp }}/wt.zip + - uses: actions/cache/restore@v6 + id: restore-ahk + with: + key: ahk-${{ env.AUTOHOTKEY_VERSION }} + path: ${{ runner.temp }}/ahk.zip + - name: Download AutoHotKey2 + if: steps.restore-ahk.outputs.cache-hit != 'true' + shell: bash + run: | + curl -L -o "$RUNNER_TEMP/ahk.zip" \ + https://github.com/AutoHotkey/AutoHotkey/releases/download/v$AUTOHOTKEY_VERSION/AutoHotkey_$AUTOHOTKEY_VERSION.zip + - uses: actions/cache/save@v6 + if: steps.restore-ahk.outputs.cache-hit != 'true' + with: + key: ahk-${{ env.AUTOHOTKEY_VERSION }} + path: ${{ runner.temp }}/ahk.zip + - name: Install AutoHotKey2 + shell: bash + run: | + mkdir -p "$RUNNER_TEMP/ahk" && + "$WINDIR/system32/tar.exe" -C "$RUNNER_TEMP/ahk" -xf "$RUNNER_TEMP/ahk.zip" && + cygpath -aw "$RUNNER_TEMP/ahk" >>$GITHUB_PATH + - uses: actions/setup-node@v6 # the hook uses node for the background process + + - name: Run UI tests + id: ui-tests + timeout-minutes: 10 + run: | + $exitCode = 0 + & "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force ui-tests\background-hook.ahk "$PWD\bg-hook" 2>&1 | Out-Default + if (!$?) { $exitCode = 1; echo "::error::Test failed!" } else { echo "::notice::Test log" } + exit $exitCode + - name: Show logs + if: always() + run: type bg-hook.log diff --git a/ui-tests/.gitattributes b/ui-tests/.gitattributes new file mode 100644 index 0000000000..7d5ccef0ca --- /dev/null +++ b/ui-tests/.gitattributes @@ -0,0 +1,2 @@ +*.ahk eol=lf +*.ps1 eol=lf diff --git a/ui-tests/background-hook.ahk b/ui-tests/background-hook.ahk new file mode 100755 index 0000000000..76d04708d2 --- /dev/null +++ b/ui-tests/background-hook.ahk @@ -0,0 +1,54 @@ +#Requires AutoHotkey v2.0 +#Include ui-test-library.ahk + +; This script is an integration test for the following scenario: +; A Git hook spawns a background process that outputs some text +; to the console even after Git has exited. + +; At some point in time, the Cygwin/MSYS2 runtime left the console +; in a state where it was not possible to navigate the history via +; CursorUp/Down, as reported in https://github.com/microsoft/git/issues/730. +; This was fixed in the Cygwin/MSYS2 runtime, but then regressed again. +; This test is meant to verify that the issue is fixed and remains so. + +SetWorkTree('git-test-background-hook') + +if not FileExist('.git/hooks') and not DirCreate('.git/hooks') + ExitWithError 'Could not create hooks directory: ' workTree + +FileAppend("#!/bin/sh`npowershell -command 'for ($i = 0; $i -lt 50; $i++) { echo $i; sleep -milliseconds 10 }' &`n", '.git/hooks/pre-commit') +if A_LastError + ExitWithError 'Could not create pre-commit hook: ' A_LastError + +Run 'wt.exe -d . ' A_ComSpec ' /d', , , &childPid +if A_LastError + ExitWithError 'Error launching CMD: ' A_LastError +Info 'Launched CMD: ' childPid +if not WinWait(A_ComSpec, , 9) + ExitWithError 'CMD window did not appear' +Info 'Got window' +WinActivate +CloseWindow := true +WinMove 0, 0 +Info 'Moved window to top left (so that the bottom is not cut off)' + +Info('Setting committer identity') +Send('git config user.name Test{Enter}git config user.email t@e.st{Enter}') + +Info('Committing') +Send('git commit --allow-empty -m zOMG{Enter}') +; Wait for the hook to finish printing +WaitForRegExInWindowsTerminal('`n49$', 'Timed out waiting for commit to finish', 'Hook finished', 100000) + +; Verify that CursorUp shows the previous command +Send('{Up}') +Sleep 150 +Text := CaptureBufferFromWindowsTerminal() +if not RegExMatch(Text, 'git commit --allow-empty -m zOMG *$') + ExitWithError 'Cursor Up did not work: ' Text +Info('Match!') + +Send('^C') +Send('exit{Enter}') +Sleep 50 +CleanUpWorkTree() \ No newline at end of file diff --git a/ui-tests/setup-portable-wt.ps1 b/ui-tests/setup-portable-wt.ps1 new file mode 100644 index 0000000000..572e6bc119 --- /dev/null +++ b/ui-tests/setup-portable-wt.ps1 @@ -0,0 +1,96 @@ +# Configures a portable Windows Terminal for the UI tests. +# +# Downloads WT if needed, then creates .portable marker and settings.json +# with exportBuffer bound to Ctrl+Shift+F12. The export file lands in the +# script's own directory (ui-tests/) so it gets uploaded as build artifact. +# +# The portable WT uses its own settings directory (next to the executable) +# so it never touches the user's installed Windows Terminal configuration. + +param( + [string]$WtVersion = $env:WT_VERSION, + [string]$DestDir = $env:TEMP +) + +if (-not $WtVersion) { $WtVersion = '1.22.11141.0' } + +$wtDir = "$DestDir\terminal-$WtVersion" +$wtExe = "$wtDir\wt.exe" + +# Download if the directory doesn't contain wt.exe yet +if (-not (Test-Path $wtExe)) { + $wtZip = "$DestDir\wt.zip" + if (-not (Test-Path $wtZip)) { + $url = "https://github.com/microsoft/terminal/releases/download/v$WtVersion/Microsoft.WindowsTerminal_${WtVersion}_x64.zip" + Write-Host "Downloading Windows Terminal $WtVersion ..." + curl.exe -fLo $wtZip $url + if ($LASTEXITCODE -ne 0) { throw "Download failed" } + } + Write-Host "Extracting ..." + & "$env:WINDIR\system32\tar.exe" -C $DestDir -xf $wtZip + if ($LASTEXITCODE -ne 0) { throw "Extract failed" } +} + +# Create .portable marker so WT reads settings from settings\ next to wt.exe +$portableMarker = "$wtDir\.portable" +if (-not (Test-Path $portableMarker)) { + Set-Content -Path $portableMarker -Value "" +} + +# Write settings.json with exportBuffer action +$settingsDir = "$wtDir\settings" +if (-not (Test-Path $settingsDir)) { New-Item -ItemType Directory -Path $settingsDir -Force | Out-Null } + +$bufferExportPath = ($PSScriptRoot + '\wt-buffer-export.txt') -replace '\\', '/' + +$settings = @" +{ + "`$schema": "https://aka.ms/terminal-profiles-schema", + "actions": [ + { + "command": { + "action": "exportBuffer", + "path": "$bufferExportPath" + }, + "id": "User.TestExportBuffer" + }, + { + "command": { "action": "copy", "singleLine": false }, + "id": "User.copy" + }, + { "command": "paste", "id": "User.paste" } + ], + "copyFormatting": "none", + "copyOnSelect": false, + "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "keybindings": [ + { "id": "User.TestExportBuffer", "keys": "ctrl+shift+f12" }, + { "id": null, "keys": "ctrl+v" }, + { "id": null, "keys": "ctrl+c" } + ], + "profiles": { + "defaults": {}, + "list": [ + { + "commandline": "%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "hidden": false, + "name": "Windows PowerShell" + } + ] + }, + "schemes": [], + "themes": [] +} +"@ + +Set-Content -Path "$settingsDir\settings.json" -Value $settings + +# Add WT to PATH if running in GitHub Actions +if ($env:GITHUB_PATH) { + $wtDir | Out-File -Append -FilePath $env:GITHUB_PATH +} + +Write-Host "Portable WT ready at: $wtDir" +Write-Host " exportBuffer path: $bufferExportPath" +Write-Host " exportBuffer key: Ctrl+Shift+F12" From 07291bf1cecbde0d7033e4532a694ec3ebcd5dfa Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 5 Jul 2025 13:54:23 +0200 Subject: [PATCH 085/102] ci(ui-tests): upload the test logs The test logs are quite interesting to have, and not only those: In case of a fatal failure, the test directory is valuable information, too. Let's always upload them as build artifacts. For convenience, let's just reuse the `ui-tests/` directory as the place to put all of those files; Technically, we do not need the files in there that are tracked by Git, but practically speaking, it is neat to have them packaged in the same `.zip` file as the test logs and stuff. Signed-off-by: Johannes Schindelin --- .github/workflows/ui-tests.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index d4f0ffd52b..26ca450dd0 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -77,11 +77,19 @@ jobs: - name: Run UI tests id: ui-tests timeout-minutes: 10 + working-directory: ui-tests run: | $exitCode = 0 - & "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force ui-tests\background-hook.ahk "$PWD\bg-hook" 2>&1 | Out-Default + & "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force background-hook.ahk "$PWD\bg-hook" 2>&1 | Out-Default if (!$?) { $exitCode = 1; echo "::error::Test failed!" } else { echo "::notice::Test log" } exit $exitCode - name: Show logs if: always() + working-directory: ui-tests run: type bg-hook.log + - name: Upload test results + if: always() + uses: actions/upload-artifact@v7 + with: + name: ui-tests-${{ matrix.os }} + path: ui-tests From 5b51e5a1e0b71d1a71fa60267d660c40cd9506fd Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 4 Jul 2025 11:48:15 +0200 Subject: [PATCH 086/102] ci(ui-tests): take a screenshot when canceled Sometimes the logs are empty and it is highly unclear what has happened. In such a scenario, a picture is indeed worth more than a thousand words. Note that this commit is more complicated than anyone would like, for two reasons: - While PowerShell is the right tool for the job, a PowerShell step in GitHub Actions will pop up a Terminal window, _hiding_ what we want to screenshot. To work around that, I tried to run things in a Bash step. _Also_ opens a Terminal window! Node.js to the rescue. - _Of course_ it is complicated to take a screenshot. The challenge is to figure out the dimensions of the screen, which should be as easy as looking at `[System.Windows.Forms.Screen]::PrimaryScreen`'s `Bounds` attribute. Easy peasy, right? No, it's not. Most machines nowadays have a _ridiculous_ resolution which is why most setups have a _zoom factor_. Getting to that factor should be trivial, by calling `GetDeviceCaps(hDC, LOGPIXELSX)`, but that's not working in modern Windows! There is a per-monitor display scaling ("DPI"). But even _that_ is hard to get at, calling `GetDpiForMonitor()` will still return 96 DPI (i.e. 100% zoom) because PowerShell is not marked as _Per-Monitor DPI Aware_. Since we do not want to write a manifest into the same directory as `powershell.exe` resides, we have to jump through yet another hoop to get that. Signed-off-by: Johannes Schindelin --- .github/workflows/ui-tests.yml | 57 ++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 26ca450dd0..d0c188cad1 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -87,6 +87,63 @@ jobs: if: always() working-directory: ui-tests run: type bg-hook.log + - name: Take screenshot, if canceled + id: take-screenshot + if: cancelled() || failure() + shell: powershell + run: | + Add-Type -TypeDefinition @" + using System; + using System.Runtime.InteropServices; + + public class DpiHelper { + [DllImport("user32.dll")] + public static extern bool SetProcessDpiAwarenessContext(IntPtr dpiContext); + + [DllImport("Shcore.dll")] + public static extern int GetDpiForMonitor(IntPtr hmonitor, int dpiType, out uint dpiX, out uint dpiY); + + [DllImport("User32.dll")] + public static extern IntPtr MonitorFromPoint(System.Drawing.Point pt, uint dwFlags); + + [DllImport("user32.dll")] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + public static uint GetDPI() { + // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4 + SetProcessDpiAwarenessContext((IntPtr)(-4)); + + uint dpiX, dpiY; + IntPtr monitor = MonitorFromPoint(new System.Drawing.Point(0, 0), 2); // MONITOR_DEFAULTTONEAREST + GetDpiForMonitor(monitor, 0, out dpiX, out dpiY); // MDT_EFFECTIVE_DPI + return (dpiX + dpiY) / 2; + } + } + "@ -ReferencedAssemblies "System.Drawing.dll" + + # First, minimize the Console window in which this script is running + $hwnd = (Get-Process -Id $PID).MainWindowHandle + $SW_MINIMIZE = 6 + + [DpiHelper]::ShowWindow($hwnd, $SW_MINIMIZE) + + # Now, get the DPI + $dpi = [DpiHelper]::GetDPI() + + # This function takes a screenshot and saves it as a PNG file + [Reflection.Assembly]::LoadWithPartialName("System.Drawing") + function screenshot([Drawing.Rectangle]$bounds, $path) { + $bmp = New-Object Drawing.Bitmap $bounds.width, $bounds.height + $graphics = [Drawing.Graphics]::FromImage($bmp) + $graphics.CopyFromScreen($bounds.Location, [Drawing.Point]::Empty, $bounds.size) + $bmp.Save($path) + $graphics.Dispose() + $bmp.Dispose() + } + Add-Type -AssemblyName System.Windows.Forms + $screen = [System.Windows.Forms.Screen]::PrimaryScreen + $bounds = [Drawing.Rectangle]::FromLTRB(0, 0, $screen.Bounds.Width * $dpi / 96, $screen.Bounds.Height * $dpi / 96) + screenshot $bounds "ui-tests/screenshot.png" - name: Upload test results if: always() uses: actions/upload-artifact@v7 From e77c651910a3cf8f145afa067029e29cf9b935e2 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 4 Jul 2025 01:32:51 +0200 Subject: [PATCH 087/102] ui-tests: verify that a `sleep` in Windows Terminal can be interrupted The Ctrl+C way to interrupt run-away processes is highly important. It was recently broken in multiple ways in the Cygwin runtime (and hence also in the MSYS2 runtime). Let's add some integration tests that will catch regressions. It is admittedly less than ideal to add _integration_ tests; While imitating exactly what the end user does looks appealing at first, excellent tests impress by how quickly they allow regressions not only to be identified but also to be fixed. Even worse: all integration tests, by virtue of working in a broader environment than, say, unit tests, incur the price of sometimes catching unactionable bugs, i.e. bugs in software that is both outside of our control as well as not the target of our testing at all. Nevertheless, seeing as Cygwin did not add any unit tests for those Ctrl+C fixes (which is understandable, given how complex testing for Ctrl+C without UI testing would be), it is better to have integration tests than no tests at all. So here goes: This commit introduces a test that verifies that the MSYS2 `sleep.exe` can be interrupted when run from PowerShell in a Windows Terminal. This was broken in v3.6.0 and fixed in 7674c51e18 (Cygwin: console: Set ENABLE_PROCESSED_INPUT when disable_master_thread, 2025-07-01). Signed-off-by: Johannes Schindelin --- .github/workflows/ui-tests.yml | 6 ++++- ui-tests/ctrl-c.ahk | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 ui-tests/ctrl-c.ahk diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index d0c188cad1..be04f65b7d 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -82,11 +82,15 @@ jobs: $exitCode = 0 & "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force background-hook.ahk "$PWD\bg-hook" 2>&1 | Out-Default if (!$?) { $exitCode = 1; echo "::error::Test failed!" } else { echo "::notice::Test log" } + & "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force ctrl-c.ahk "$PWD\ctrl-c" 2>&1 | Out-Default + if (!$?) { $exitCode = 1; echo "::error::Ctrl+C Test failed!" } else { echo "::notice::Ctrl+C Test log" } exit $exitCode - name: Show logs if: always() working-directory: ui-tests - run: type bg-hook.log + run: | + type bg-hook.log + type ctrl-c.log - name: Take screenshot, if canceled id: take-screenshot if: cancelled() || failure() diff --git a/ui-tests/ctrl-c.ahk b/ui-tests/ctrl-c.ahk new file mode 100644 index 0000000000..992df0ac5d --- /dev/null +++ b/ui-tests/ctrl-c.ahk @@ -0,0 +1,48 @@ +#Requires AutoHotkey v2.0 +#Include ui-test-library.ahk + +SetWorkTree('git-test-ctrl-c') + +powerShellPath := EnvGet('SystemRoot') . '\System32\WindowsPowerShell\v1.0\powershell.exe' +Run 'wt.exe -d . "' powerShellPath '"', , , &childPid +if A_LastError + ExitWithError 'Error launching PowerShell: ' A_LastError +Info 'Launched PowerShell: ' childPid +; Sadly, `WinWait('ahk_pid ' childPid)` does not work because the Windows Terminal window seems +; to be owned by the `wt.exe` process that launched. +; +; Probably should use the trick mentioned in +; https://www.autohotkey.com/boards/viewtopic.php?p=580081&sid=a40d0ce73efff728ffa6b4573dff07b9#p580081 +; where the `before` variable is assigned `WinGetList(winTitle).Length` before the `Run` command, +; and a `Loop` is used to wait until [`WinGetList()`](https://www.autohotkey.com/docs/v2/lib/WinGetList.htm) +; returns a different length, in which case the first array element is the new window. +; +; Also: This is crying out loud to be refactored into a function and then also used in `background-hook.ahk`! +hwnd := WinWait(powerShellPath, , 9) +if not hwnd + ExitWithError 'PowerShell window did not appear' +Info 'Got window' +WinActivate +CloseWindow := true +WinMove 0, 0 +Info 'Moved window to top left (so that the bottom is not cut off)' + +WaitForRegExInWindowsTerminal('PS [A-Z]:.*>[ `n`r]*$', 'Timed out waiting for PowerShell to start', 'PowerShell prompt appeared', 30000) + +; sleep test +Sleep 1500 +; The `:;` is needed to force Git to call this via the shell, otherwise `/usr/bin/` would not resolve. +Send('git -c alias.sleep="{!}:;/usr/bin/sleep" sleep 15{Enter}') +Sleep 500 +; interrupt sleep; Ideally we'd call `Send('^C')` but that would too quick on GitHub Actions' runners. +; The idea for this work-around comes from https://www.reddit.com/r/AutoHotkey/comments/aok10s/comment/eg57e81/. +Send '{Ctrl down}{c down}' +Sleep 50 +Send '{c up}{Ctrl up}' +Sleep 150 +; Wait for the `^C` tell-tale that is the PowerShell prompt to appear +WaitForRegExInWindowsTerminal('>[ `n`r]*$', 'Timed out waiting for interrupt', 'Sleep was interrupted as desired') + +Send('exit{Enter}') +Sleep 50 +CleanUpWorkTree() \ No newline at end of file From 27e9e465403daae5aaf2c44ee52806a895f7717d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 4 Jul 2025 01:44:33 +0200 Subject: [PATCH 088/102] ui-tests: verify that interrupting clones via SSH works This was the actual use case that was broken and necessitated the fix in 7674c51e18 (Cygwin: console: Set ENABLE_PROCESSED_INPUT when disable_master_thread, 2025-07-01). It does require an SSH server, which Git for Windows no longer ships. Therefore, this test uses the `sshd.exe` of OpenSSH for Windows (https://github.com/powershell/Win32-OpenSSH) in conjunction with Git for Windows' `ssh.exe` (because using OpenSSH for Windows' variant of `ssh.exe` would not exercise the MSYS2 runtime and therefore not demonstrate a regression, should it surface in the future). To avoid failing the test because OpenSSH for Windows is not available, the test case is guarded by the environment variable `OPENSSH_FOR_WINDOWS_DIRECTORY` which needs to point to a directory that contains a working `sshd.exe`. Signed-off-by: Johannes Schindelin --- .github/workflows/ui-tests.yml | 28 ++++++++ ui-tests/ctrl-c.ahk | 118 +++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index be04f65b7d..7a8ae7922d 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -10,6 +10,7 @@ on: env: AUTOHOTKEY_VERSION: 2.0.19 WT_VERSION: 1.22.11141.0 + WIN32_OPENSSH_VERSION: 9.8.3.0p2-Preview jobs: ui-tests: @@ -73,6 +74,27 @@ jobs: "$WINDIR/system32/tar.exe" -C "$RUNNER_TEMP/ahk" -xf "$RUNNER_TEMP/ahk.zip" && cygpath -aw "$RUNNER_TEMP/ahk" >>$GITHUB_PATH - uses: actions/setup-node@v6 # the hook uses node for the background process + - uses: actions/cache/restore@v6 + id: restore-win32-openssh + with: + key: win32-openssh-${{ env.WIN32_OPENSSH_VERSION }} + path: ${{ runner.temp }}/win32-openssh.zip + - name: Download Win32-OpenSSH + if: steps.restore-win32-openssh.outputs.cache-hit != 'true' + shell: bash + run: | + curl -fLo "$RUNNER_TEMP/win32-openssh.zip" \ + https://github.com/PowerShell/Win32-OpenSSH/releases/download/v$WIN32_OPENSSH_VERSION/OpenSSH-Win64.zip + - uses: actions/cache/save@v6 + if: steps.restore-win32-openssh.outputs.cache-hit != 'true' + with: + key: win32-openssh-${{ env.WIN32_OPENSSH_VERSION }} + path: ${{ runner.temp }}/win32-openssh.zip + - name: Unpack Win32-OpenSSH + shell: bash + run: | + "$WINDIR/system32/tar.exe" -C "$RUNNER_TEMP" -xvf "$RUNNER_TEMP/win32-openssh.zip" && + echo "OPENSSH_FOR_WINDOWS_DIRECTORY=$(cygpath -aw "$RUNNER_TEMP/OpenSSH-Win64")" >>$GITHUB_ENV - name: Run UI tests id: ui-tests @@ -148,6 +170,12 @@ jobs: $screen = [System.Windows.Forms.Screen]::PrimaryScreen $bounds = [Drawing.Rectangle]::FromLTRB(0, 0, $screen.Bounds.Width * $dpi / 96, $screen.Bounds.Height * $dpi / 96) screenshot $bounds "ui-tests/screenshot.png" + - name: Stop SSH server + if: always() + shell: powershell + run: | + Get-Process sshd -ErrorAction SilentlyContinue | + ForEach-Object { Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue } - name: Upload test results if: always() uses: actions/upload-artifact@v7 diff --git a/ui-tests/ctrl-c.ahk b/ui-tests/ctrl-c.ahk index 992df0ac5d..a42e4bb6fd 100644 --- a/ui-tests/ctrl-c.ahk +++ b/ui-tests/ctrl-c.ahk @@ -43,6 +43,124 @@ Sleep 150 ; Wait for the `^C` tell-tale that is the PowerShell prompt to appear WaitForRegExInWindowsTerminal('>[ `n`r]*$', 'Timed out waiting for interrupt', 'Sleep was interrupted as desired') +; Clone via SSH test; Requires an OpenSSH for Windows `sshd.exe` whose directory needs to be specified via +; the environment variable `OPENSSH_FOR_WINDOWS_DIRECTORY`. The clone will still be performed via Git's +; included `ssh.exe`, to exercise the MSYS2 runtime (which these UI tests are all about). + +openSSHPath := EnvGet('OPENSSH_FOR_WINDOWS_DIRECTORY') +if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) { + Info('Generate 26M of data') + RunWait('git init --bare -b main large.git', '', 'Hide') + RunWait('git --git-dir=large.git -c alias.c="!(' . + 'printf \"reset refs/heads/main\\n\"; ' . + 'seq 100000 | ' . + 'sed \"s|.*|blob\\nmark :&\\ndata < 1234& +0000\\ndata < deadline + ExitWithError 'sshd did not write its PidFile within 60 seconds' + Sleep 500 + } + } + + ; Set up SSH server + Info('Generating host key') + RunWait('git -c alias.c="!ssh-keygen -b 4096 -f ssh_host_rsa_key -N \"\"" c', '', 'Hide') + if A_LastError + ExitWithError 'Error generating host key: ' A_LastError + AdjustPermissions('ssh_host_rsa_key') + AdjustPermissions('ssh_host_rsa_key.pub') + Info('Generating client key') + RunWait('git -c alias.c="!ssh-keygen -f id_rsa -N \"\"" c', '', 'Hide') + if A_LastError + ExitWithError 'Error generating client key: ' A_LastError + AdjustPermissions('id_rsa') + AdjustPermissions('id_rsa.pub') + FileAppend('Port 2322`n' . + 'HostKey "' . workTree . '\ssh_host_rsa_key"`n' . + 'AuthorizedKeysFile "' . workTree . '\id_rsa.pub"`n' . + 'LogLevel VERBOSE`n' . + 'PidFile "' . workTree . '\sshd.pid"`n', + 'sshd_config') + sshdOptions := '-f "' . workTree . '\sshd_config" -D -E "' . workTree . '\sshd.log"' + + ; Start SSH server + Info('Starting SSH server') + Run(openSSHPath . '\sshd.exe ' . sshdOptions, '', 'Hide', &sshdPID) + if A_LastError + ExitWithError 'Error starting SSH server: ' A_LastError + Info('Started SSH server: ' sshdPID) + + Info('Starting clone') + workTreeMSYS := RunWaitOne('git -c alias.cygpath="!cygpath" cygpath -u "' . workTree . '"') + sshOptions := '-i ' . workTreeMSYS . '/id_rsa -p 2322 -T ' . + '-o UserKnownHostsFile=' . workTreeMSYS . '/known_hosts ' . + '-o StrictHostKeyChecking=accept-new ' + ; The `--upload-pack` option is needed because OpenSSH for Windows' default shell + ; is `cmd.exe`, which does not handle single-quoted strings as Git expects. + ; An heavy-handed alternative would be to require PowerShell to be configured via + ; HKLM:\SOFTWARE\OpenSSH's DefaultShell property, for full details see + ; https://github.com/PowerShell/Win32-OpenSSH/wiki/Setting-up-a-Git-server-on-Windows-using-Git-for-Windows-and-Win32_OpenSSH + ; + ; The username is needed because by default, on domain-joined machines MSYS2's + ; `ssh.exe` prefixes the username with the domain name. + cloneOptions := '--upload-pack="powershell git upload-pack" ' . + EnvGet('USERNAME') . '@localhost:' . workTree . '\large.git large-clone' + WaitForSshd() + Send('git -c core.sshCommand="ssh ' . sshOptions . '" clone ' . cloneOptions . '{Enter}') + Sleep 50 + Info('Waiting for clone to start') + WinActivate('ahk_id ' . hwnd) + WaitForRegExInWindowsTerminal('remote: ', 'Timed out waiting for clone to start', 'Clone started', 15000, 'ahk_id ' . hwnd) + Info('Trying to interrupt clone') + Send('^C') ; interrupt clone + Sleep 150 + WaitForRegExInWindowsTerminal('`nfatal: (.*`r?`n){1,3}PS .*>[ `n`r]*$', 'Timed out waiting for clone to be interrupted', 'clone was interrupted as desired') + + if DirExist(workTree . '\large-clone') + ExitWithError('`large-clone` was unexpectedly not deleted on interrupt') + + for proc in ComObjGet('winmgmts:').ExecQuery('SELECT ProcessId, Name, ExecutablePath FROM Win32_Process WHERE Name LIKE "sshd%.exe"') { + if (proc.ExecutablePath != '' and InStr(proc.ExecutablePath, openSSHPath) > 0) { + Info('Stopping ' . proc.Name . ' (PID ' . proc.ProcessId . ')') + try { + ProcessClose proc.ProcessId + ProcessWaitClose proc.ProcessId, 5 + } + } + } +} + Send('exit{Enter}') Sleep 50 CleanUpWorkTree() \ No newline at end of file From 17ee78dedeb43c60c1f87d670e7e949dd30f5213 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 5 Jul 2025 20:01:34 +0200 Subject: [PATCH 089/102] ci(ui-tests): exclude the large repository from the build artifact In the previous commit, I added a new UI test that generates a somewhat large repository for testing the clone via SSH. Since that repository is created in the test directory, that would inflate the `ui-tests` build artifact rather dramatically. So let's create the repository outside of that directory. Signed-off-by: Johannes Schindelin --- .github/workflows/ui-tests.yml | 1 + ui-tests/ctrl-c.ahk | 15 ++++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 7a8ae7922d..850d7ead4a 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -104,6 +104,7 @@ jobs: $exitCode = 0 & "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force background-hook.ahk "$PWD\bg-hook" 2>&1 | Out-Default if (!$?) { $exitCode = 1; echo "::error::Test failed!" } else { echo "::notice::Test log" } + $env:LARGE_FILES_DIRECTORY = "${env:RUNNER_TEMP}\large" & "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force ctrl-c.ahk "$PWD\ctrl-c" 2>&1 | Out-Default if (!$?) { $exitCode = 1; echo "::error::Ctrl+C Test failed!" } else { echo "::notice::Ctrl+C Test log" } exit $exitCode diff --git a/ui-tests/ctrl-c.ahk b/ui-tests/ctrl-c.ahk index a42e4bb6fd..86d0fac43f 100644 --- a/ui-tests/ctrl-c.ahk +++ b/ui-tests/ctrl-c.ahk @@ -50,8 +50,13 @@ WaitForRegExInWindowsTerminal('>[ `n`r]*$', 'Timed out waiting for interrupt', ' openSSHPath := EnvGet('OPENSSH_FOR_WINDOWS_DIRECTORY') if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) { Info('Generate 26M of data') - RunWait('git init --bare -b main large.git', '', 'Hide') - RunWait('git --git-dir=large.git -c alias.c="!(' . + largeFilesDirectory := EnvGet('LARGE_FILES_DIRECTORY') + if largeFilesDirectory == '' + largeFilesDirectory := workTree . '-large-files' + largeGitRepoPath := largeFilesDirectory . '\large.git' + largeGitClonePath := largeFilesDirectory . '\large-clone' + RunWait('git init --bare -b main "' . largeGitRepoPath . '"', '', 'Hide') + RunWait('git --git-dir="' . largeGitRepoPath . '" -c alias.c="!(' . 'printf \"reset refs/heads/main\\n\"; ' . 'seq 100000 | ' . 'sed \"s|.*|blob\\nmark :&\\ndata <[ `n`r]*$', 'Timed out waiting for clone to be interrupted', 'clone was interrupted as desired') - if DirExist(workTree . '\large-clone') + if DirExist(largeGitClonePath) ExitWithError('`large-clone` was unexpectedly not deleted on interrupt') for proc in ComObjGet('winmgmts:').ExecQuery('SELECT ProcessId, Name, ExecutablePath FROM Win32_Process WHERE Name LIKE "sshd%.exe"') { From 43a57fed993e2f13af2429ea958c8cb25b748be6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 4 Jul 2025 01:58:20 +0200 Subject: [PATCH 090/102] ui-tests: add `ping` interrupt test The fixes of 7674c51e18 (Cygwin: console: Set ENABLE_PROCESSED_INPUT when disable_master_thread, 2025-07-01) were unfortunately not complete; There were still a couple of edge cases where Ctrl+C was unable to interrupt processes. Let's add a demonstration of that issue. Signed-off-by: Johannes Schindelin --- ui-tests/ctrl-c.ahk | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ui-tests/ctrl-c.ahk b/ui-tests/ctrl-c.ahk index 86d0fac43f..728d1bc424 100644 --- a/ui-tests/ctrl-c.ahk +++ b/ui-tests/ctrl-c.ahk @@ -43,6 +43,17 @@ Sleep 150 ; Wait for the `^C` tell-tale that is the PowerShell prompt to appear WaitForRegExInWindowsTerminal('>[ `n`r]*$', 'Timed out waiting for interrupt', 'Sleep was interrupted as desired') +; ping test (`cat.exe` should be interrupted, too) +Send('git -c alias.c="{!}cat | /c/windows/system32/ping -t localhost" c{Enter}') +Sleep 500 +WaitForRegExInWindowsTerminal('Pinging ', 'Timed out waiting for pinging to start', 'Pinging started', 10000) +Send('^C') ; interrupt ping and cat +Sleep 150 +; Wait for the `^C` tell-tale to appear +WaitForRegExInWindowsTerminal('Control-C', 'Timed out waiting for pinging to be interrupted', 'Pinging was interrupted as desired') +; Wait for the `^C` tell-tale that is the PowerShell prompt to appear +WaitForRegExInWindowsTerminal('>[ `n`r]*$', 'Timed out waiting for `cat.exe` to be interrupted', '`cat.exe` was interrupted as desired') + ; Clone via SSH test; Requires an OpenSSH for Windows `sshd.exe` whose directory needs to be specified via ; the environment variable `OPENSSH_FOR_WINDOWS_DIRECTORY`. The clone will still be performed via Git's ; included `ssh.exe`, to exercise the MSYS2 runtime (which these UI tests are all about). From 299f9a487b2c75701f31dcc10c0cc135343e8341 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 5 Jul 2025 18:46:47 +0200 Subject: [PATCH 091/102] ui-tests: do verify the SSH hang fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In 0ae6a6fa74 (Cygwin: pipe: Fix SSH hang with non-cygwin pipe reader, 2025-06-27), a quite problematic bug was fixed where somewhat large-ish repositories could not be cloned via SSH anymore. This fix was not accompanied by a corresponding test case in Cygwin's test suite, i.e. there is no automated way to ensure that there won't be any regressions on that bug (and therefore it would fall onto end users to deal with those). This constitutes what Michael C. Feathers famously characterized as "legacy code" in his book "Working Effectively with Legacy Code": To me, legacy code is simply code without tests. I've gotten some grief for this definition. What do tests have to do with whether code is bad? To me, the answer is straightforward, and it is a point that I elaborate throughout the book: Code without tests is bad code. It doesn’t matter how well written it is; it doesn’t matter how pretty or object-oriented or well-encapsulated it is. With tests, we can change the behavior of our code quickly and verifiably. Without them, we really don’t know if our code is getting better or worse. Just to drive this point home, let me pull out Exhibit A: The bug fix in question, which is the latest (and hopefully last) commit in a _long_ chain of bug fixes that fix bugs introduced by preceding bug fixes: - 9e4d308cd5 (Cygwin: pipe: Adopt FILE_SYNCHRONOUS_IO_NONALERT flag for read pipe., 2021-11-10) fixed a bug where Cygwin hung by mistake while piping output from one .NET program as input to another .NET program (potentially introduced by 365199090c (Cygwin: pipe: Avoid false EOF while reading output of C# programs., 2021-11-07), which was itself a bug fix). It introduced a bug that was fixed by... - fc691d0246 (Cygwin: pipe: Make sure to set read pipe non-blocking for cygwin apps., 2024-03-11). Which introduced a bug that was purportedly fixed by... - 7ed9adb356 (Cygwin: pipe: Switch pipe mode to blocking mode by default, 2024-09-05). Which introduced a bug that was fixed by... - cbfaeba4f7 (Cygwin: pipe: Fix incorrect write length in raw_write(), 2024-11-06). Which introduced a bug that was fixed by... the SSH hang fix in 0ae6a6fa74 (Cygwin: pipe: Fix SSH hang with non-cygwin pipe reader, 2025-06-27). There is not only the common thread here that each of these bug fixes introduced a new bug, but also the common thread that none of the commits introduced new test cases into the test suite that could potentially have helped prevent future breakages in this code. So let's at least add an integration test here. Side note: I am quite unhappy with introducing integration tests. I know there are a lot of fans out there, but I cannot help wondering whether they favor the convenience of writing tests quickly over the vast cost of making debugging any regression a highly cumbersome and unenjoyable affair (try single-stepping through a test case that requires several processes to be orchestrated in unison). Also, integration tests have the large price of introducing moving parts outside the code base that is actually to be tested, opening the door for breakages caused by software (or infrastructure, think: network glitches!) that are completely outside the power or responsibility of the poor engineer tasked with fixing the breakages. Nevertheless, I have been unable despite days of trying to wrap my head around the issue to figure out a way to reproduce the `fhandler_pipe_fifo::raw_write()` hang without involving a MINGW `git.exe` and an MSYS2/Cygwin `ssh.exe`. So: It's the best I could do with any reasonable amount of effort. It's better to have integration tests that would demonstrate regressions than not having any tests for that at all. Signed-off-by: Johannes Schindelin --- ui-tests/ctrl-c.ahk | 256 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 227 insertions(+), 29 deletions(-) diff --git a/ui-tests/ctrl-c.ahk b/ui-tests/ctrl-c.ahk index 728d1bc424..db41841bdc 100644 --- a/ui-tests/ctrl-c.ahk +++ b/ui-tests/ctrl-c.ahk @@ -32,10 +32,15 @@ WaitForRegExInWindowsTerminal('PS [A-Z]:.*>[ `n`r]*$', 'Timed out waiting for Po ; sleep test Sleep 1500 ; The `:;` is needed to force Git to call this via the shell, otherwise `/usr/bin/` would not resolve. -Send('git -c alias.sleep="{!}:;/usr/bin/sleep" sleep 15{Enter}') -Sleep 500 +Send('git -c alias.sleep="{!}:;echo __SLEEP_STARTED__;' . + '/usr/bin/sleep" sleep 15{Enter}') +WaitForRegExInWindowsTerminal( + '(^|`n)__SLEEP_STARTED__`r?`n', + 'Timed out waiting for sleep to start', 'Sleep started', + 10000, 'ahk_id ' . hwnd) ; interrupt sleep; Ideally we'd call `Send('^C')` but that would too quick on GitHub Actions' runners. ; The idea for this work-around comes from https://www.reddit.com/r/AutoHotkey/comments/aok10s/comment/eg57e81/. +WinActivate('ahk_id ' . hwnd) Send '{Ctrl down}{c down}' Sleep 50 Send '{c up}{Ctrl up}' @@ -91,14 +96,14 @@ if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) { ExitWithError 'Could not add admin read permission from ' . path . ': ' A_LastError } - WaitForSshd() { + WaitForSshd(expectedPID) { deadline := A_TickCount + 60000 while true { if FileExist('sshd.pid') { content := '' try content := Trim(FileRead('sshd.pid'), ' `t`r`n') - if content != '' { + if content == expectedPID && ProcessExist(expectedPID) { Info('sshd is accepting connections (PID ' . content . ')') return } @@ -109,6 +114,94 @@ if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) { } } + StartSshd(openSSHPath, sshdOptions, sshdPIDs) { + try FileDelete('sshd.pid') + Run(openSSHPath . '\sshd.exe ' . sshdOptions, '', 'Hide', &pid) + if A_LastError + ExitWithError 'Error starting SSH server: ' A_LastError + sshdPIDs.Push(pid) + Info('Started SSH server: ' . pid) + WaitForSshd(pid) + return pid + } + + StopSshd(pid, openSSHPath, sshdPIDs) { + if !pid + return true + proc := FindProcess(pid) + executablePath := '' + if proc + try executablePath := proc.ExecutablePath + if executablePath == openSSHPath . '\sshd.exe' { + Info('Stopping sshd.exe (PID ' . pid . ')') + try ProcessClose(pid) + try ProcessWaitClose(pid, 5) + } + if !ProcessExist(pid) { + loop sshdPIDs.Length { + if sshdPIDs[A_Index] == pid { + sshdPIDs.RemoveAt(A_Index) + break + } + } + } + return !ProcessExist(pid) + } + + CleanUpSshdProcesses(sshdPIDs, openSSHPath, *) { + for pid in sshdPIDs.Clone() + StopSshd(pid, openSSHPath, sshdPIDs) + } + + FindProcess(pid) { + query := 'SELECT ProcessId, ParentProcessId, Name, CommandLine, ' . + 'ExecutablePath FROM Win32_Process WHERE ProcessId = ' . pid + for proc in ComObjGet('winmgmts:').ExecQuery(query) + return proc + return 0 + } + + ProcessMatches(pid, name, marker) { + proc := FindProcess(pid) + if !proc || proc.Name != name + return false + commandLine := '' + try commandLine := proc.CommandLine + return InStr(commandLine, marker) + } + + ; Count the regular files (not directories) below `dir`, recursing into + ; subdirectories and hidden entries such as a `.git` folder. + CountFilesRecursively(dir) { + count := 0 + Loop Files, dir . '\*', 'FR' + count++ + return count + } + + WatchSshStarts() { + query := 'SELECT * FROM Win32_ProcessStartTrace ' . + 'WHERE ProcessName = "ssh.exe"' + return ComObjGet('winmgmts:').ExecNotificationQuery(query) + } + + WaitForCloneSsh(events, keyPath) { + deadline := A_TickCount + 15000 + while A_TickCount < deadline { + try event := events.NextEvent(deadline - A_TickCount) + catch + break + ssh := FindProcess(event.ProcessID) + if !ssh + continue + sshCommandLine := '' + try sshCommandLine := ssh.CommandLine + if InStr(sshCommandLine, keyPath) + return ssh.ProcessId + } + return 0 + } + ; Set up SSH server Info('Generating host key') RunWait('git -c alias.c="!ssh-keygen -b 4096 -f ssh_host_rsa_key -N \"\"" c', '', 'Hide') @@ -129,13 +222,14 @@ if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) { 'PidFile "' . workTree . '\sshd.pid"`n', 'sshd_config') sshdOptions := '-f "' . workTree . '\sshd_config" -D -E "' . workTree . '\sshd.log"' + sshdPIDs := [] + sshdCleanup := CleanUpSshdProcesses.Bind( + sshdPIDs, openSSHPath) + OnExit(sshdCleanup) ; Start SSH server Info('Starting SSH server') - Run(openSSHPath . '\sshd.exe ' . sshdOptions, '', 'Hide', &sshdPID) - if A_LastError - ExitWithError 'Error starting SSH server: ' A_LastError - Info('Started SSH server: ' sshdPID) + sshdPID := StartSshd(openSSHPath, sshdOptions, sshdPIDs) Info('Starting clone') workTreeMSYS := RunWaitOne('git -c alias.cygpath="!cygpath" cygpath -u "' . workTree . '"') @@ -152,31 +246,135 @@ if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) { ; `ssh.exe` prefixes the username with the domain name. cloneOptions := '--upload-pack="powershell git upload-pack" "' . EnvGet('USERNAME') . '@localhost:' . largeGitRepoPath . '" "' . largeGitClonePath . '"' - WaitForSshd() - Send('git -c core.sshCommand="ssh ' . sshOptions . '" clone ' . cloneOptions . '{Enter}') - Sleep 50 - Info('Waiting for clone to start') + sshStartEvents := WatchSshStarts() WinActivate('ahk_id ' . hwnd) - WaitForRegExInWindowsTerminal('remote: ', 'Timed out waiting for clone to start', 'Clone started', 15000, 'ahk_id ' . hwnd) + Send('git -c core.sshCommand="ssh ' . sshOptions . '" clone ' . + cloneOptions . '{Enter}') + cloneSshPID := WaitForCloneSsh( + sshStartEvents, workTreeMSYS . '/id_rsa') + if !cloneSshPID + ExitWithError 'Timed out waiting for clone ssh.exe' + Info('Clone ssh.exe started: ' . cloneSshPID) Info('Trying to interrupt clone') - Send('^C') ; interrupt clone - Sleep 150 - WaitForRegExInWindowsTerminal('`nfatal: (.*`r?`n){1,3}PS .*>[ `n`r]*$', 'Timed out waiting for clone to be interrupted', 'clone was interrupted as desired') - - if DirExist(largeGitClonePath) - ExitWithError('`large-clone` was unexpectedly not deleted on interrupt') - - for proc in ComObjGet('winmgmts:').ExecQuery('SELECT ProcessId, Name, ExecutablePath FROM Win32_Process WHERE Name LIKE "sshd%.exe"') { - if (proc.ExecutablePath != '' and InStr(proc.ExecutablePath, openSSHPath) > 0) { - Info('Stopping ' . proc.Name . ' (PID ' . proc.ProcessId . ')') - try { - ProcessClose proc.ProcessId - ProcessWaitClose proc.ProcessId, 5 - } + if !ProcessMatches(cloneSshPID, 'ssh.exe', workTreeMSYS . '/id_rsa') + ExitWithError 'Clone completed before Ctrl+C could be sent' + ; Interrupt the clone. A bare `Send('^C')` is too quick to be delivered + ; reliably on GitHub Actions' runners (see the sleep interrupt above), and + ; even the deliberate key-down/up sequence is occasionally lost to a + ; focus/scheduling race. A missed interrupt lets the clone run to completion + ; (its ssh.exe only exits once the ~26M transfer finishes), so keep + ; re-issuing the Ctrl+C, re-focusing the window each time, until ssh.exe + ; actually exits. + deadline := A_TickCount + 15000 + while ProcessMatches( + cloneSshPID, 'ssh.exe', workTreeMSYS . '/id_rsa') && + A_TickCount < deadline { + WinActivate('ahk_id ' . hwnd) + Send '{Ctrl down}{c down}' + Sleep 50 + Send '{c up}{Ctrl up}' + checkDeadline := A_TickCount + 600 + while ProcessExist(cloneSshPID) && A_TickCount < checkDeadline + Sleep 20 + } + if ProcessMatches(cloneSshPID, 'ssh.exe', workTreeMSYS . '/id_rsa') + ExitWithError 'Clone ssh.exe did not exit after Ctrl+C' + Info('clone was interrupted as desired') + + ; Interrupting `git clone` makes it run its `remove_junk` cleanup, which + ; unlinks every file of the partial clone. On Windows that cleanup races + ; with the still-terminating child processes: their delete-pending file + ; handles (and CWDs) keep the now file-less directories busy, so git's + ; `rmdir` of the empty scaffolding fails and it gives up, permanently + ; leaving behind an empty `large-clone\.git\{objects,refs}` skeleton. That + ; benign leftover is not a completed clone, so it must not fail the test: + ; the interrupt is already proven by the clone's `ssh.exe` having exited + ; (above) and by the clone content being gone. Wait for every file to + ; disappear (tolerating empty directories), fail only if actual clone + ; content survives (i.e. the clone was not aborted), then remove any empty + ; scaffolding ourselves so the verification clone below starts clean. + deadline := A_TickCount + 5000 + while DirExist(largeGitClonePath) && + CountFilesRecursively(largeGitClonePath) > 0 && + A_TickCount < deadline + Sleep 10 + if DirExist(largeGitClonePath) { + remainingFiles := CountFilesRecursively(largeGitClonePath) + if remainingFiles > 0 + ExitWithError('`large-clone` still contained ' . remainingFiles . + ' file(s) after interrupt (clone was not aborted)') + ; Only empty scaffolding remains; drop it so the verification clone + ; below can create the target afresh. The directories may stay briefly + ; busy while the interrupted clone's children finish exiting, so retry. + deadline := A_TickCount + 5000 + while DirExist(largeGitClonePath) && A_TickCount < deadline { + try DirDelete(largeGitClonePath, true) + if !DirExist(largeGitClonePath) + break + Sleep 50 } } + + ; Now verify that the SSH-based clone actually works and does not hang + Info('Re-starting SSH server') + if !StopSshd(sshdPID, openSSHPath, sshdPIDs) + ExitWithError 'Could not stop SSH server before restart' + sshdPID := StartSshd(openSSHPath, sshdOptions, sshdPIDs) + + Info('Starting clone') + retries := 5 + cloneResultMarker := 'GIT_CLONE_EXIT_CODE=' + Loop retries { + WinActivate('ahk_id ' . hwnd) + Send('git -c core.sshCommand="ssh ' . sshOptions . '" clone ' . + cloneOptions . '; Write-Output "' . cloneResultMarker . + '$LASTEXITCODE"{Enter}') + Sleep 500 + Info('Waiting for clone to finish (attempt ' . A_Index . '/' . retries . ')') + WinActivate('ahk_id ' . hwnd) + matchObj := WaitForRegExInWindowsTerminal( + cloneResultMarker . '([0-9]+)', + 'Timed out waiting for clone to finish', + 'Clone command completed', 15000, 'ahk_id ' . hwnd) + + if matchObj[1] == '0' + break + if A_Index == retries + ExitWithError('Clone failed after ' . retries . + ' attempts (exit code ' . matchObj[1] . ')') + Info('Clone failed with exit code ' . matchObj[1] . + ', restarting SSH server and retrying...') + if DirExist(largeGitClonePath) + DirDelete(largeGitClonePath, true) + if !StopSshd(sshdPID, openSSHPath, sshdPIDs) + ExitWithError 'Could not stop SSH server before retry' + sshdPID := StartSshd(openSSHPath, sshdOptions, sshdPIDs) + Info('Restarted SSH server: ' . sshdPID) + } + + if not DirExist(largeGitClonePath) + ExitWithError('`large-clone` did not work?!?') + + CleanUpSshdProcesses(sshdPIDs, openSSHPath) + if sshdPIDs.Length + ExitWithError 'Could not stop all SSH servers' + OnExit(sshdCleanup, 0) } -Send('exit{Enter}') -Sleep 50 +; Close the PowerShell window. As with the Ctrl+C interrupts above, a single +; `Send('exit{Enter}')` is occasionally lost to a focus/scheduling race on +; GitHub Actions' runners, which would leave the Windows Terminal window (and +; its OpenConsole/PowerShell processes) behind. Re-issue the exit, re-focusing +; the window each time, until it actually closes; the leading `{Enter}` flushes +; any partial command a half-delivered attempt might have left on the prompt. +deadline := A_TickCount + 20000 +while WinExist('ahk_id ' . hwnd) && A_TickCount < deadline { + try WinActivate('ahk_id ' . hwnd) + if WinExist('ahk_id ' . hwnd) + Send('{Enter}exit{Enter}') + WinWaitClose('ahk_id ' . hwnd, , 3) +} +if WinExist('ahk_id ' . hwnd) + ExitWithError 'PowerShell window did not close' +Info 'PowerShell window closed' CleanUpWorkTree() \ No newline at end of file From d0fb14144d174bead9ae39a51c23cca80a2d3e22 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 10 Oct 2025 12:50:07 +0200 Subject: [PATCH 092/102] ui-tests: minimize Log window On hosted GitHub Actions runners, there is always this Log window visible on the Desktop, and due to some magic logic, this window is sometimes in the foreground on the `windows-2025` runners. Let's minimize it so that it is out of the way and does not interfere with the AutoHotKey-based UI tests. Signed-off-by: Johannes Schindelin --- .github/workflows/ui-tests.yml | 7 +++++++ ui-tests/minimize-log-window.ahk | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 ui-tests/minimize-log-window.ahk diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 850d7ead4a..90ecf70e27 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -96,6 +96,13 @@ jobs: "$WINDIR/system32/tar.exe" -C "$RUNNER_TEMP" -xvf "$RUNNER_TEMP/win32-openssh.zip" && echo "OPENSSH_FOR_WINDOWS_DIRECTORY=$(cygpath -aw "$RUNNER_TEMP/OpenSSH-Win64")" >>$GITHUB_ENV + - name: Minimize existing Log window + working-directory: ui-tests + run: | + $exitCode = 0 + & "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force minimize-log-window.ahk "$PWD\minimize-log-window" 2>&1 | Out-Default + if (!$?) { $exitCode = 1; echo "::error::Failed to minimize Log window!" } else { echo "::notice::Minimized Log window" } + exit $exitCode - name: Run UI tests id: ui-tests timeout-minutes: 10 diff --git a/ui-tests/minimize-log-window.ahk b/ui-tests/minimize-log-window.ahk new file mode 100644 index 0000000000..d7b116bc69 --- /dev/null +++ b/ui-tests/minimize-log-window.ahk @@ -0,0 +1,21 @@ +#Requires AutoHotkey v2.0 +#Include ui-test-library.ahk + +for hwnd in WinGetList() +{ + title := WinGetTitle(hwnd) + if title != "" + { + FileAppend 'Got window ' . hwnd . '`n', '*' + try { + exe := WinGetProcessName(hwnd) + } catch as e { + FileAppend 'Could not get executable for hwnd ' . hwnd . ': ' . e.Message . '`n', '*' + exe := "" + } + title := WinGetTitle(hwnd) + FileAppend 'Got window ' . hwnd . ' ah_exe ' . exe . ' title ' . title . '`n', '*' + + WinMinimize(hwnd) + } +} From 43670d1037493cde925012d44887fa3e587b6ad0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2026 23:51:07 +0100 Subject: [PATCH 093/102] ui-tests: add mintty launch and capture helpers to the library The existing UI test infrastructure only supports Windows Terminal, but the keystroke reordering bug reported in https://github.com/git-for-windows/git/issues/5632 manifests most reliably in mintty, which uses a different PTY code path. To write a reproducer for that bug, we need library functions that can launch mintty and read back what it displayed. An initial attempt used mintty's `-l` flag to write a terminal log file, then read back that log with ANSI escape sequences stripped. This approach turned out to be unreliable: mintty buffers its log output, so content that is already visible on screen (such as the `$ ` prompt) may not have been flushed to the log file yet. Polling for a prompt that is already displayed but not yet logged leads to an indefinite wait. Instead, LaunchMintty() configures mintty's Ctrl+F5 keybinding to trigger the `export-html` action, which writes an HTML snapshot of the current screen to a file. This is instantaneous and always reflects exactly what is on screen. The function uses window-class enumeration to identify the newly-created mintty window among any pre-existing instances and returns its handle. CaptureBufferFromMintty() sends Ctrl+F5 to trigger the export, reads the resulting HTML file, extracts the `` content, strips HTML tags, and decodes common entities to return plain text suitable for substring matching. It accepts an optional window title to activate the correct mintty instance before sending the keystroke. Note that AHK's ControlSend cannot be used here because mintty passes the raw keycodes through to the terminal session rather than interpreting them as window-level shortcuts, so WinActivate followed by Send is the only way to trigger the export action. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- ui-tests/ui-test-library.ahk | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/ui-tests/ui-test-library.ahk b/ui-tests/ui-test-library.ahk index e6d261ed45..645bcd0d82 100644 --- a/ui-tests/ui-test-library.ahk +++ b/ui-tests/ui-test-library.ahk @@ -116,4 +116,70 @@ WaitForRegExInWindowsTerminal(regex, errorMessage, successMessage, timeout := 50 ExitWithError errorMessage } } +} + +; Launch mintty with HTML export support. Returns the window handle. +; Ctrl+F5 is bound to export-html; the file is written to /mintty-export.html. +LaunchMintty(extraArgs := '') { + exportFile := A_ScriptDir . '\mintty-export.html' + savePattern := StrReplace(A_ScriptDir, '\', '/') '/mintty-export' + minttyClass := 'ahk_class mintty' + existing := Map() + for h in WinGetList(minttyClass) + existing[h] := true + + cmd := 'mintty.exe -o "KeyFunctions=C+F5:export-html" -o "SaveFilename=' savePattern '"' + if extraArgs != '' + cmd .= ' ' extraArgs + cmd .= ' -' + Run cmd, , , &childPid + Info 'Launched mintty, PID: ' childPid + + hwnd := 0 + deadline := A_TickCount + 10000 + while A_TickCount < deadline + { + for h in WinGetList(minttyClass) + { + if !existing.Has(h) + { + hwnd := h + break 2 + } + } + Sleep 100 + } + if !hwnd + ExitWithError 'New mintty window did not appear' + WinActivate('ahk_id ' hwnd) + Info 'Found new mintty: ' hwnd + return hwnd +} + +; Trigger Ctrl+F5 to export mintty's screen as HTML, read it, strip tags, +; and return the plain text. +CaptureBufferFromMintty(winTitle := '') { + static exportFile := A_ScriptDir . '\mintty-export.html' + if FileExist(exportFile) + FileDelete exportFile + if winTitle != '' + WinActivate winTitle + Send '^{F5}' + deadline := A_TickCount + 3000 + while !FileExist(exportFile) && A_TickCount < deadline + Sleep 50 + if !FileExist(exportFile) + return '' + Sleep 100 + html := FileRead(exportFile) + ; Extract body content only (skip CSS in