From 76b2f9d37acf6eb4bfd90c44a1d3e869a4841efd Mon Sep 17 00:00:00 2001 From: Vizonex Date: Wed, 8 Apr 2026 22:17:02 -0500 Subject: [PATCH] fix process stdio when frozen as gui a executable --- winloop/handles/process.pyx | 12 +- winloop/includes/compat.h | 37 ++++ winloop/includes/system.pxd | 5 +- winloop/shlex.pxd | 38 ---- winloop/shlex.pyx | 346 ------------------------------------ 5 files changed, 49 insertions(+), 389 deletions(-) delete mode 100644 winloop/shlex.pxd delete mode 100644 winloop/shlex.pyx diff --git a/winloop/handles/process.pyx b/winloop/handles/process.pyx index 3aad40a..2c8f7c2 100644 --- a/winloop/handles/process.pyx +++ b/winloop/handles/process.pyx @@ -386,6 +386,9 @@ DEF _CALL_PIPE_CONNECTION_LOST = 1 DEF _CALL_PROCESS_EXITED = 2 DEF _CALL_CONNECTION_LOST = 3 +cdef int WINDOWS_EXE_FROZEN = system.__IS_WINDOWS_EXE_FROZEN() + + @cython.no_gc_clear cdef class UVProcessTransport(UVProcess): @@ -439,11 +442,14 @@ cdef class UVProcessTransport(UVProcess): else: self._pending_calls.append((_CALL_PIPE_DATA_RECEIVED, fd, data)) - # TODO: https://github.com/Vizonex/Winloop/issues/126 bug fix for uvloop - # Might need a special implementation for subprocess.Popen._get_handles() - # but can't seem to wrap my head around how to go about doing it. + cdef _file_redirect_stdio(self, int fd): + if WINDOWS_EXE_FROZEN: + # SEE: https://github.com/Vizonex/Winloop/issues/126 + # use devnull instead... + return self._file_devnull() + fd = os_dup(fd) os_set_inheritable(fd, True) self._close_after_spawn(fd) diff --git a/winloop/includes/compat.h b/winloop/includes/compat.h index d42a8f1..47f77c2 100644 --- a/winloop/includes/compat.h +++ b/winloop/includes/compat.h @@ -1,6 +1,7 @@ #include #include #include +#include // intptr_t #ifndef _WIN32 #include #include @@ -179,3 +180,39 @@ void PyOS_AfterFork_Child() { #endif +/* There is a bug with CX-Freeze on windows when compiled + * to an exe this tries to fix it by seeing if alternate + * workarounds like DEVNULL need to be provided. + * SEE: https://github.com/Vizonex/Winloop/issues/126 */ + +#ifdef _WIN32 +/* CPython version might be slower so will use our own, audits can be a costly thing, */ + +static int _get_std_handle(DWORD std_handle){ + /* This might be a leak, IDK... CloseHandle just seems to crash it.*/ + HANDLE handle = GetStdHandle(std_handle); + if (handle == INVALID_HANDLE_VALUE){ + goto error; + } + /* We don't need this handle open we just want to know + if windows wants to play nice or not. executable vs not executable. */ + int windows_misbehaved = (handle == NULL) ? 1: 0; + /* if handle == 0 use DEVNULL as backup instead otherwise use the other method. */ + return windows_misbehaved; +error: + /* if handle == -1 throw an error */ + PyErr_SetFromWindowsErr(GetLastError()); + return -1; +} + +/* Because these are macros it's very easy to make a workaround for unix. */ +#define __IS_WINDOWS_EXE_FROZEN() _get_std_handle(STD_INPUT_HANDLE) +#else +/* On Unix these are not needed, but we define it anyways so the +compiler doesn't wind up throwing a fit about it */ + +#define __IS_WINDOWS_EXE_FROZEN() 0 /* NOPE */ + +#endif + + diff --git a/winloop/includes/system.pxd b/winloop/includes/system.pxd index 96554a7..231f03d 100644 --- a/winloop/includes/system.pxd +++ b/winloop/includes/system.pxd @@ -1,4 +1,4 @@ -from libc.stdint cimport int8_t, uint64_t +from libc.stdint cimport int8_t, uint64_t, uintptr_t cdef extern from "includes/compat.h" nogil: @@ -65,7 +65,8 @@ cdef extern from "includes/compat.h" nogil: int epoll_ctl(int epfd, int op, int fd, epoll_event *event) object MakeUnixSockPyAddr(sockaddr_un *addr) - + int __IS_WINDOWS_EXE_FROZEN() except -1 + cdef extern from "includes/fork_handler.h": uint64_t MAIN_THREAD_ID diff --git a/winloop/shlex.pxd b/winloop/shlex.pxd deleted file mode 100644 index 1129c6e..0000000 --- a/winloop/shlex.pxd +++ /dev/null @@ -1,38 +0,0 @@ - - -# TODO: In the future I would like to see this module start using -# const char* and other simillar optimizations, will start with -# this for now - Vizonex - -cdef class shlex: - cdef: - readonly str commenters - readonly str wordchars - readonly str whitespace - readonly str escape - readonly str quotes - readonly str escapedquotes - readonly bint whitespace_split - readonly str infile - readonly object instream - readonly str source - readonly int debug - readonly int lineno - readonly str token - readonly str eof - readonly bint posix - object _punctuation_chars - object state - object pushback - object filestack - - cpdef object get_token(self) - cpdef object push_token(self, object tok) - cpdef object read_token(self) - cpdef tuple sourcehook(self, str newfile) - cpdef object push_source(self, object newstream, object newfile=*) - cpdef object pop_source(self) - cpdef object error_leader(self, object infile=*, object lineno=*) - - # Custom function used for saving time with splitting data up. - cpdef list split(self, str s) diff --git a/winloop/shlex.pyx b/winloop/shlex.pyx deleted file mode 100644 index 11d035c..0000000 --- a/winloop/shlex.pyx +++ /dev/null @@ -1,346 +0,0 @@ - -# Shlex was moved on over to Cython in order to decrease some -# costly calls. Shlex was added to fix a bug with adding shell data -# from windows. -# SEE Comment : https://github.com/agronholm/anyio/pull/960#issuecomment-3217705210 - -import sys -from io import StringIO -from collections import deque -import os - - -# Modified parser from shlex library gets added to winloop as a class -# member for helping with parsing shell components. - -# If this inspires any CPython maintainers to implement this -# in C please make a Capsule for it, it reduces the costly amounts -# of things I have to add in here :) - -# Dear Other Devs who want this, -# If you want this code in your cython projects Just copy and paste -# this code from slex.pxd and shlex.pyx and link to where you got -# it from and you should be safe to use it elsewhere I'm only -# needing it to fix a bug and to stop a few possible bottlenecks -# with expensive cython calls being made. - -cdef class shlex: - "A lexical analyzer class for simple shell-like syntaxes." - - def __cinit__( - self, - object instream=None, - object infile=None, - bint posix=False, - # object because this variable likes jumping types so much. - object punctuation_chars=False - ): - if isinstance(instream, str): - instream = StringIO(instream) - if instream is not None: - self.instream = instream - self.infile = infile - else: - self.instream = sys.stdin - self.infile = None - self.posix = posix - if posix: - self.eof = None - else: - self.eof = '' - self.commenters = '#' - self.wordchars = ('abcdfeghijklmnopqrstuvwxyz' - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_') - if self.posix: - self.wordchars += ('ßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ' - 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ') - self.whitespace = ' \t\r\n' - self.whitespace_split = False - self.quotes = '\'"' - self.escape = '\\' - self.escapedquotes = '"' - self.state = ' ' - self.pushback = deque() - self.lineno = 1 - self.debug = 0 - self.token = '' - self.filestack = deque() - self.source = None - if not punctuation_chars: - punctuation_chars = '' - elif punctuation_chars is True: - punctuation_chars = '();<>|&' - self._punctuation_chars = punctuation_chars - if punctuation_chars: - # _pushback_chars is a push back queue used by lookahead logic - self._pushback_chars = deque() - # these chars added because allowed in file names, args, wildcards - self.wordchars += '~-./*?=' - # remove any punctuation chars from wordchars - t = self.wordchars.maketrans(dict.fromkeys(punctuation_chars)) - self.wordchars = self.wordchars.translate(t) - - @property - def punctuation_chars(self): - return self._punctuation_chars - - cpdef object push_token(self, object tok): - "Push a token onto the stack popped by the get_token method" - if self.debug >= 1: - print("shlex: pushing token " + repr(tok)) - self.pushback.appendleft(tok) - - cpdef object push_source(self, object newstream, object newfile=None): - "Push an input source onto the lexer's input source stack." - if isinstance(newstream, str): - newstream = StringIO(newstream) - self.filestack.appendleft((self.infile, self.instream, self.lineno)) - self.infile = newfile - self.instream = newstream - self.lineno = 1 - if self.debug: - if newfile is not None: - print('shlex: pushing to file %s' % (self.infile,)) - else: - print('shlex: pushing to stream %s' % (self.instream,)) - - cpdef object pop_source(self): - "Pop the input source stack." - self.instream.close() - (self.infile, self.instream, self.lineno) = self.filestack.popleft() - if self.debug: - print('shlex: popping to %s, line %d' \ - % (self.instream, self.lineno)) - self.state = ' ' - - cpdef object get_token(self): - "Get a token from the input stream (or from stack if it's nonempty)" - if self.pushback: - tok = self.pushback.popleft() - if self.debug >= 1: - print("shlex: popping token " + repr(tok)) - return tok - # No pushback. Get a token. - raw = self.read_token() - # Handle inclusions - if self.source is not None: - while raw == self.source: - spec = self.sourcehook(self.read_token()) - if spec: - (newfile, newstream) = spec - self.push_source(newstream, newfile) - raw = self.get_token() - # Maybe we got EOF instead? - while raw == self.eof: - if not self.filestack: - return self.eof - else: - # kill immediately - self.pop_source() - break - # BUG - # raw = self.get_token() - # Neither inclusion nor EOF - if self.debug >= 1: - if raw != self.eof: - print("shlex: token=" + repr(raw)) - else: - print("shlex: token=EOF") - return raw - - cpdef object read_token(self): - cdef bint quoted = False - cdef str nextchar - cdef str escapedstate = ' ' - cdef str result - while True: - if self.punctuation_chars and self._pushback_chars: - nextchar = self._pushback_chars.pop() - else: - assert self.instream - nextchar = self.instream.read(1) - - if nextchar == '\n': - self.lineno += 1 - if self.debug >= 3: - print("shlex: in state %r I see character: %r" % (self.state, - nextchar)) - if self.state is None: - self.token = '' # past end of file - break - elif self.state == ' ': - if not nextchar: - self.state = None # end of file - break - elif nextchar in self.whitespace: - if self.debug >= 2: - print("shlex: I see whitespace in whitespace state") - if self.token or (self.posix and quoted): - break # emit current token - else: - continue - elif nextchar in self.commenters: - self.instream.readline() - self.lineno += 1 - elif self.posix and nextchar in self.escape: - escapedstate = 'a' - self.state = nextchar - elif nextchar in self.wordchars: - self.token = nextchar - self.state = 'a' - elif nextchar in self.punctuation_chars: - self.token = nextchar - self.state = 'c' - elif nextchar in self.quotes: - if not self.posix: - self.token = nextchar - self.state = nextchar - elif self.whitespace_split: - self.token = nextchar - self.state = 'a' - else: - self.token = nextchar - if self.token or (self.posix and quoted): - break # emit current token - else: - continue - elif self.state in self.quotes: - quoted = True - if not nextchar: # end of file - if self.debug >= 2: - print("shlex: I see EOF in quotes state") - # XXX what error should be raised here? - raise ValueError("No closing quotation") - if nextchar == self.state: - if not self.posix: - self.token += nextchar - self.state = ' ' - break - else: - self.state = 'a' - elif (self.posix and nextchar in self.escape and self.state - in self.escapedquotes): - escapedstate = self.state - self.state = nextchar - else: - self.token += nextchar - elif self.state in self.escape: - if not nextchar: # end of file - if self.debug >= 2: - print("shlex: I see EOF in escape state") - # XXX what error should be raised here? - raise ValueError("No escaped character") - # In posix shells, only the quote itself or the escape - # character may be escaped within quotes. - if (escapedstate in self.quotes and - nextchar != self.state and nextchar != escapedstate): - self.token += self.state - self.token += nextchar - self.state = escapedstate - elif self.state in ('a', 'c'): - if not nextchar: - self.state = None # end of file - break - elif nextchar in self.whitespace: - if self.debug >= 2: - print("shlex: I see whitespace in word state") - self.state = ' ' - if self.token or (self.posix and quoted): - break # emit current token - else: - continue - elif nextchar in self.commenters: - self.instream.readline() - self.lineno += 1 - if self.posix: - self.state = ' ' - if self.token or (self.posix and quoted): - break # emit current token - else: - continue - elif self.state == 'c': - if nextchar in self.punctuation_chars: - self.token += nextchar - else: - if nextchar not in self.whitespace: - self._pushback_chars.append(nextchar) - self.state = ' ' - break - elif self.posix and nextchar in self.quotes: - self.state = nextchar - elif self.posix and nextchar in self.escape: - escapedstate = 'a' - self.state = nextchar - elif (nextchar in self.wordchars or nextchar in self.quotes - or (self.whitespace_split and - nextchar not in self.punctuation_chars)): - self.token += nextchar - else: - if self.punctuation_chars: - self._pushback_chars.append(nextchar) - else: - self.pushback.appendleft(nextchar) - if self.debug >= 2: - print("shlex: I see punctuation in word state") - self.state = ' ' - if self.token or (self.posix and quoted): - break # emit current token - else: - continue - result = self.token - self.token = '' - if self.posix and not quoted and result == '': - result = None - if self.debug > 1: - if result: - print("shlex: raw token=" + repr(result)) - else: - print("shlex: raw token=EOF") - return result - - cpdef tuple sourcehook(self, str newfile): - "Hook called on a filename to be sourced." - if newfile[0] == '"': - newfile = newfile[1:-1] - # This implements cpp-like semantics for relative-path inclusion. - if isinstance(self.infile, str) and not os.path.isabs(newfile): - newfile = os.path.join(os.path.dirname(self.infile), newfile) - return (newfile, open(newfile, "r")) - - cpdef object error_leader(self, object infile=None, object lineno=None): - "Emit a C-compiler-like, Emacs-friendly error-message leader." - if infile is None: - infile = self.infile - if lineno is None: - lineno = self.lineno - return "\"%s\", line %d: " % (infile, lineno) - - def __iter__(self): - return self - - def __next__(self): - token = self.get_token() - if token == self.eof: - raise StopIteration - return token - - # This part is completely custom and this is - # to save us all from the burden of reallocating to the shelx - # parser multiple times which saves a few headaches in the process. - cpdef list split(self, str s): - self.push_source(s) - cdef list tokens = [] - cdef str token = self.get_token() - while token: - # strip quotes to prevent accidental cmd parsing failures... - # The real reason as to why tests with anyio would fail revolved around - # bad argument parsing If we don't strip the quotes out what ultimately - # occurs is that the running something like python - - # SEE: https://github.com/Vizonex/Winloop/pull/72 - # it assumes it's a string and as a result no stdin input occurs... - tokens.append(token.strip("\"")) - token = self.get_token() - return tokens - -