Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Windows] Python Installer crashed using nsis 3.08 #526

Closed
2 tasks done
JinYongWu opened this issue Jul 19, 2022 · 14 comments · Fixed by #563
Closed
2 tasks done

[Windows] Python Installer crashed using nsis 3.08 #526

JinYongWu opened this issue Jul 19, 2022 · 14 comments · Fixed by #563
Labels
locked [bot] locked due to inactivity mitigated a workaround has been identified source::community catch-all for issues filed by community members type::bug describes erroneous operation, use severity::* to classify the type

Comments

@JinYongWu
Copy link

Checklist

  • I added a descriptive title
  • I searched open reports and couldn't find a duplicate

What happened?

We created a customized Python toolkit installer using constructor, the installer worked well when using nsis 3.01.
Recently we upgraded nsis to 3.08 which provides the capability to sign the uninstaller exe.
After switched to nsis 3.08(with no any other additional changes), we experienced crashes during installation process.
Here are the high level steps to observe the crashes:

  1. Generate the customized Python toolkit installer using constructor (and nsis 3.08);
  2. Install the generated installer for the first time, noticed that everything is installed correctly;
  3. Uninstall the Python toolkit from "Add & Remove programs";
  4. Run the installer again;
  5. This time the installation window disappeared after a while, and checked the installation folder, there were just a few folders and the packages were not extracted, the subfolders and files are as below:
 conda-meta [folder]
 Lib [folder]
 pkgs [folder]
 _conda.exe [file]
  1. Checked the task manager, noticed that there are 3 conda processes running under Background processes - even the installation window disappeared;
  2. Checked the windows Event Viewer, noticed that there were some faulting errors for the mentioned installer:
Faulting application name: PythonToolkitInstallTool.exe, version: 2022.1.0.0, time stamp: 0x614f9b5a
Faulting module name: nsExec.dll, version: 0.0.0.0, time stamp: 0x614f9a06
Exception code: 0xc0000005
Fault offset: 0x00001564
Faulting process id: 0x1ac
Faulting application start time: 0x01d88c6c9f7ae701
Faulting application path: C:\Distr\PythonToolkitInstallTool.exe
Faulting module path: C:\Users\demo\AppData\Local\Temp\nsh4DF0.tmp\nsExec.dll
Report Id: b1533623-83c7-48e8-9778-48d2f9aad5be
Faulting package full name: 
Faulting package-relative application ID: 

and information report:

Fault bucket 1397276649508340299, type 1
Event Name: APPCRASH
Response: Not available
Cab Id: 0

Problem signature:
P1: PythonToolkitInstallTool.exe
P2: 2022.1.0.0
P3: 614f9b5a
P4: nsExec.dll
P5: 0.0.0.0
P6: 614f9a06
P7: c0000005
P8: 00001564
P9: 
P10: 

Attached files:
\\?\C:\ProgramData\Microsoft\Windows\WER\Temp\WERCDE8.tmp.dmp
\\?\C:\ProgramData\Microsoft\Windows\WER\Temp\WERCFBE.tmp.WERInternalMetadata.xml
\\?\C:\ProgramData\Microsoft\Windows\WER\Temp\WERD06B.tmp.xml
\\?\C:\ProgramData\Microsoft\Windows\WER\Temp\WERD078.tmp.csv
\\?\C:\ProgramData\Microsoft\Windows\WER\Temp\WERD0A8.tmp.txt

These files may be available here:
\\?\C:\ProgramData\Microsoft\Windows\WER\ReportArchive\AppCrash_PythonToolkitIns_9f5d981163b5c43653c0d5beb4183a411697a096_7e09f96b_feb4c539-e392-41b1-972a-a430b1dafb39

Analysis symbol: 
Rechecking for solution: 0
Report Id: b1533623-83c7-48e8-9778-48d2f9aad5be
Report Status: 268435456
Hashed bucket: 7211a04ea917b3d7a3641fb5aae4824b
Cab Guid: 0

If we only change the nsis to version 3.01, the installer generated with same configuration worked fine for above steps. We compared and made sure that the issue happened as soon as we only upgrade nsis package to 3.08.

Conda Info

No response

Conda Config

No response

Conda list

No response

Additional Context

No response

@JinYongWu JinYongWu added the type::bug describes erroneous operation, use severity::* to classify the type label Jul 19, 2022
@scottwsides
Copy link

I havent checked nsis version... but my windows conda installers are intermittently failing for the last couple of weeks. I always use the latest and greatest conda constructor version on windows.

@jaimergp
Copy link
Contributor

jaimergp commented Jul 23, 2022

I am seeing some issues in the CI too, where some Windows jobs suddenly hang indefinitely. Python version doesn't seem to be a factor.

@jaimergp
Copy link
Contributor

First time it hang was May 11th (commit be1b1f6), just one day after nsis 3.08 was made available on defaults.

So yes, it looks like there are some sporadic problems where the installer might not run successfully (but sometimes it does?). I'd recommend pinning nsis to 3.01 in the meantime.

@jaimergp
Copy link
Contributor

Maybe related: actions/runner-images#4739 (comment)

@scottwsides
Copy link

scottwsides commented Jul 23, 2022

Yeah... that timeframe sounds about right. I figured out the problem I was seeing. I was substituting a custom default user path for windows and it was missing a trailing ''. This reliably causes the 'intermittent' behavior (I know that sounds weird). This also explains when my users would type in an install path and leave out the trailing slash. If you use the menu to create a directory then it substitutes the backslash... but if you type in manually you can break it. I'm not super familiar with NSIS but this sounds like something it could/should check for and compensate. The really bad thing is... that if that trailing slash isn't there... no error message gets printed... the installer hard crashes. Took me a LONG time to figure out what was going on.

@jlstevens
Copy link
Contributor

jlstevens commented Jul 27, 2022

I'd recommend pinning nsis to 3.01 in the meantime.

This is good advice for people who don't need any of the new NSIS features though some users may need things like the
!uninstfinalize command which is only available in >3.01.

I'll also add that I couldn't get a windows installer to complete without hanging when built against 3.08 while the installer built against 3.01 worked just fine (just to confirm the issue as described above).

@JinYongWu
Copy link
Author

Maybe related: actions/virtual-environments#4739 (comment)

I also tried to put "Unicode false" at the beginning of nsis script to use NSIS 3.08. I got similar errors caused by that some plugins were not loaded, i.e. UAC.dll.

@jaimergp
Copy link
Contributor

For now I am going to issue some repodata patches so constructor only uses NSIS 3.01. Next release will hopefully address the problem.

@jaimergp
Copy link
Contributor

jaimergp commented Sep 5, 2022

I am taking another look at this issue and I can't manage to reproduce the problem in my VM. I am trying to use NSIS log builds to try and catch where the error is coming from, so the fact that I don't run into this issue with the log builds means that it could be a packaging problem?

Let's see if this fixes your problem temporarily, @JinYongWu:

  1. Create a new constructor environment with conda create -n constructor-log-builds constructor nsis=3.08 --copy. We need 'copy' so we can overwrite stuff safely.
  2. Download the NSIS log builds from here.
  3. Extract the ZIP and overwrite the contents of %CONDA_PREFIX%\NSIS with the contents of the ZIP.
  4. Run constructor path/to/construct.yml as usual.

@jaimergp
Copy link
Contributor

jaimergp commented Sep 5, 2022

Other things to try:

  • Since the error seems to originate from nsExec.dll, that means the problem lies with the nsExec::* calls. This plugin allows us to run subprocesses without spawning a CMD window. If it crashes there, it might be either a problem with the launching itself, or with the collection of the output.
  • nsExec has changed "a lot" from 3.01 to 3.08:
Diff for `nsexec.c` between versions
--- /Users/jrodriguez/Downloads/nsis-3.01-src/Contrib/nsExec/nsexec.c   2022-09-05 12:36:55.000000000 +0200
+++ /Users/jrodriguez/Downloads/nsis-3.08-src/Contrib/nsExec/nsexec.c   2022-09-05 12:36:55.000000000 +0200
@@ -1,5 +1,6 @@
 /*
-Copyright (c) 2002 Robert Rainwater <rrainwater@yahoo.com>
+Copyright (C) 2002 Robert Rainwater <rrainwater@yahoo.com>
+Copyright (C) 2002-2021 Nullsoft and Contributors
 
 This software is provided 'as-is', without any express or implied
 warranty.  In no event will the authors be held liable for any damages
@@ -17,8 +18,6 @@
    misrepresented as being the original software.
   3. This notice may not be removed or altered from any source distribution.
 
-Unicode support by Jim Park -- 08/24/2007
-
 */
 #include <windows.h>
 #include <commctrl.h>
@@ -32,114 +31,166 @@
 #endif //~ _MSC_VER >= 1500
 #endif //~ _MSC_VER
 
-#ifndef true
-#define true TRUE
-#endif
-#ifndef false
-#define false FALSE
-#endif
+#define TAB_REPLACE _T("        ")
+#define TAB_REPLACE_SIZE (sizeof(TAB_REPLACE) - sizeof(_T("")))
+#define TAB_REPLACE_CCH (TAB_REPLACE_SIZE / sizeof(_T("")))
+enum { MODE_IGNOREOUTPUT = 0, MODE_LINES = 1, MODE_STACK = 2 };
 
 #define LOOPTIMEOUT  100
 HWND g_hwndParent;
 HWND g_hwndList;
+HINSTANCE g_hInst;
 
 void ExecScript(BOOL log);
-void LogMessage(const TCHAR *pStr, BOOL bOEM);
 TCHAR *my_strstr(TCHAR *a, TCHAR *b);
 unsigned int my_atoi(TCHAR *s);
 int WINAPI AsExeWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow);
 
 void __declspec(dllexport) Exec(HWND hwndParent, int string_size, TCHAR *variables, stack_t **stacktop) {
-  g_hwndParent=hwndParent;
+  g_hwndParent = hwndParent;
   EXDLL_INIT();
-  {
-    ExecScript(0);
-  }
+  ExecScript(MODE_IGNOREOUTPUT);
 }
 
 void __declspec(dllexport) ExecToLog(HWND hwndParent, int string_size, TCHAR *variables, stack_t **stacktop) {
-  g_hwndParent=hwndParent;
+  g_hwndParent = hwndParent;
   EXDLL_INIT();
-  {
-    ExecScript(1);
-  }
+  ExecScript(MODE_LINES);
 }
 
 void __declspec(dllexport) ExecToStack(HWND hwndParent, int string_size, TCHAR *variables, stack_t **stacktop) {
-  g_hwndParent=hwndParent;
+  g_hwndParent = hwndParent;
   EXDLL_INIT();
-  {
-    ExecScript(2);
-  }
+  ExecScript(MODE_STACK);
 }
 
-HINSTANCE g_hInst;
 BOOL WINAPI DllMain(HINSTANCE hInst, ULONG ul_reason_for_call, LPVOID lpReserved) {
   g_hInst = hInst;
   return TRUE;
 }
 
-#define TAB_REPLACE _T("        ")
-#define TAB_REPLACE_SIZE (sizeof(TAB_REPLACE)-1)
+static BOOL IsLeadSurrogateUTF16(unsigned short c) { return c >= 0xd800 && c <= 0xdbff; }
+static BOOL IsTrailSurrogateUTF16(unsigned short c) { return c >= 0xdc00 && c <= 0xdfff; }
 
-BOOL IsWOW64() {
-  typedef BOOL (WINAPI *LPFN_ISWOW64PROCESS) (HANDLE, PBOOL);
-  BOOL wow64;
-  LPFN_ISWOW64PROCESS fnIsWow64Process;
-
-  fnIsWow64Process = (LPFN_ISWOW64PROCESS) GetProcAddress(
-    GetModuleHandle(_T("kernel32")), "IsWow64Process");
-
-  if (fnIsWow64Process != NULL) {
-    if (fnIsWow64Process(GetCurrentProcess(), &wow64)) {
-      return wow64;
+static PWSTR MyCharNext(PCWSTR p) 
+{
+  // Note: This is wrong for surrogate pair combining characters but CharNextW does 
+  // not support surrogate pairs correctly so we have to manually handle the pairs.
+  if (!p[0]) return (PWSTR) p;
+  if (IsLeadSurrogateUTF16(p[0]) && IsTrailSurrogateUTF16(p[1])) return (PWSTR) p + 2; // Current is a surrogate pair, we incorrectly assume that it is not followed by combining characters.
+  if (IsLeadSurrogateUTF16(p[1]) && IsTrailSurrogateUTF16(p[2])) return (PWSTR) p + 1; // Next is a surrogate pair, we incorrectly assume that it is not a combining character for the current character.
+  return (CharNextW)(p);
+}
+#define CharNextW MyCharNext
+
+static void TruncateStringUTF16LE(LPWSTR Buffer, SIZE_T Length, LPCWSTR Overflow, SIZE_T lenOver) {
+  if (Length) {
+    LPWSTR p = &Buffer[Length - 1];
+    UINT stripBaseCharIfCuttingCombining = TRUE;
+
+    // CharNextW is buggy on XP&2003 but we don't care enough to call GetStringTypeW (http://archives.miloush.net/michkap/archive/2005/01/30/363420.html)
+    if (stripBaseCharIfCuttingCombining && lenOver) {
+      WCHAR buf[] = { *p, Overflow[0], lenOver > 1 ? Overflow[1] : L' ', L'\0' };
+      for (;;) {
+        BOOL comb = CharNextW(buf) > buf + 1;
+        if (!comb || p < Buffer) break;
+        *((WORD*)((BYTE*)&buf[1])) = *((WORD*)((BYTE*)&buf[0]));
+        buf[0] = *p;
+        *p-- = L'\0';
+      }
+    }
+
+    if (IsLeadSurrogateUTF16(*p)) {
+      *p = L'\0'; // Avoid incomplete pair
     }
   }
+}
+
+static void TruncateStringMB(UINT Codepage, LPSTR Buffer, SIZE_T Length, unsigned short OverflowCh) {
+  if (Length) {
+    CHAR *p = &Buffer[Length - 1], buf[] = { *p, ' ', ' ', '\0' };
 
+    if (CharNextExA(Codepage, buf, 0) > buf + 1) { // Remove incomplete DBCS character?
+      *p = '\0';
+    }
+  }
+}
+
+static BOOL IsWOW64() {
+#ifdef _WIN64
   return FALSE;
+#else
+  typedef BOOL (WINAPI*ISWOW64PROCESS)(HANDLE, BOOL*);
+  ISWOW64PROCESS pfIsWow64Process;
+  typedef BOOL (WINAPI*ISWOW64PROCESS2)(HANDLE, USHORT*, USHORT*);
+  ISWOW64PROCESS2 pfIsWow64Process2;
+  HANDLE hProcess = GetCurrentProcess();
+  HMODULE hK32 = GetModuleHandleA("KERNEL32");
+  UINT_PTR retval;
+  USHORT appmach, image_file_machine_unknown = 0;
+  CHAR funcnam[16]
+#if defined(_MSC_VER) && (_MSC_VER-0 <= 1400)
+    = "IsWow64Process2"; // MOVSD * 4
+#else
+    ; lstrcpyA(funcnam, "IsWow64Process2");
+#endif
+  pfIsWow64Process2 = (ISWOW64PROCESS2) GetProcAddress(hK32, funcnam);
+  if (pfIsWow64Process2 && pfIsWow64Process2(hProcess, &appmach, NULL)) {
+    retval = image_file_machine_unknown != appmach;
+  }
+  else {
+    BOOL wow64;
+    pfIsWow64Process = (ISWOW64PROCESS) GetProcAddress(hK32, (funcnam[14] = '\0', funcnam));
+    retval = (UINT_PTR) pfIsWow64Process;
+    if (pfIsWow64Process && (retval = pfIsWow64Process(hProcess, &wow64))) {
+      retval = wow64;
+    }
+  }
+  return (BOOL) (UINT) retval;
+#endif
 }
 
-/**
- * Convert the ansiStr if storing ANSI strings, otherwise, assume that the
- * string is wide and don't convert, but straight copy.
- * @param ansiStr [in]  the suspected ANSI string.
- * @param wideBuf [out] the buffer to write to.
- * @param cnt     [in]  the size of widebuf in wchar_t's.
- * @return true, if ASCII, false if suspected as wide.
- */
-BOOL WideConvertIfASCII(const char* ansiStr, int strLen, WCHAR* wideBuf, int cnt)
-{
-   BOOL rval = FALSE;
-   wideBuf[0] = 0;
-   if (lstrlenA(ansiStr) == strLen)
-   {
-      MultiByteToWideChar(CP_ACP, 0, ansiStr, -1, wideBuf, cnt);
-      rval = TRUE;
-   }
-   else
-   {
-      // Going to assume that it's a wide char array.
-      lstrcpyW(wideBuf, (const WCHAR*) ansiStr);
-   }
-
-   return rval;
-}
-
-void ExecScript(int log) {
-  TCHAR szRet[128] = _T("");
-  TCHAR meDLLPath[MAX_PATH];    
-  TCHAR *executor;
-  TCHAR *g_exec;
+// Tim Kosse's LogMessage
+#ifdef UNICODE
+static void LogMessage(const TCHAR *pStr, BOOL bOEM) {
+#else
+static void LogMessage(TCHAR *pStr, BOOL bOEM) {
+#endif
+  LVITEM item;
+  int nItemCount;
+  if (!g_hwndList) return;
+  //if (!*pStr) return;
+#ifndef UNICODE
+  if (bOEM == TRUE) OemToCharBuff(pStr, pStr, lstrlen(pStr));
+#endif
+  nItemCount=(int) SendMessage(g_hwndList, LVM_GETITEMCOUNT, 0, 0);
+  item.mask=LVIF_TEXT;
+  item.pszText=(TCHAR *)pStr;
+  item.cchTextMax=0;
+  item.iItem=nItemCount, item.iSubItem=0;
+  ListView_InsertItem(g_hwndList, &item);
+  ListView_EnsureVisible(g_hwndList, item.iItem, 0);
+}
+
+
+void ExecScript(int mode) {
+  TCHAR szRet[128];
+  TCHAR meDLLPath[MAX_PATH];
+  TCHAR *g_exec, *executor;
   TCHAR *pExec;
-  unsigned int g_to;
-  BOOL bOEM;
+  int ignoreData = mode == MODE_IGNOREOUTPUT;
+  int logMode = mode == MODE_LINES, stackMode = mode == MODE_STACK;
+  unsigned int to, tabExpandLength = logMode ? TAB_REPLACE_CCH : 0, codepage;
+  BOOL bOEM, forceNarrowInput = FALSE;
+
+  *szRet = _T('\0');
 
   if (!IsWOW64()) {
     TCHAR* p;
     int nComSpecSize;
 
     nComSpecSize = GetModuleFileName(g_hInst, meDLLPath, MAX_PATH) + 2; // 2 chars for quotes
-    g_exec = (TCHAR *)GlobalAlloc(GPTR, sizeof(TCHAR)*(g_stringsize+nComSpecSize+2)); // 1 for space, 1 for null
+    g_exec = (TCHAR *)GlobalAlloc(GPTR, sizeof(TCHAR) * (g_stringsize+nComSpecSize+2)); // 1 for space, 1 for null
     p = meDLLPath + nComSpecSize - 2; // point p at null char of meDLLPath
     *g_exec = _T('"');
     executor = g_exec + 1;
@@ -198,18 +249,18 @@
     pExec++;
   } else {
     executor = NULL;
-    g_exec = (TCHAR *)GlobalAlloc(GPTR, sizeof(TCHAR)*(g_stringsize+1)); // 1 for NULL
+    g_exec = (TCHAR *)GlobalAlloc(GPTR, sizeof(TCHAR) * (g_stringsize+1)); // 1 for NULL
     pExec = g_exec;
   }
 
-  g_to = 0;      // default is no timeout
-  bOEM = FALSE;  // default is no OEM->ANSI conversion
+  to = 0; // default is no timeout
+  bOEM = FALSE; // default is no OEM->ANSI conversion
 
   g_hwndList = NULL;
   
   // g_hwndParent = the caller, usually NSIS installer.
   if (g_hwndParent) // The window class name for dialog boxes is "#32770"
-    g_hwndList = FindWindowEx(FindWindowEx(g_hwndParent,NULL,_T("#32770"),NULL),NULL,_T("SysListView32"),NULL);
+    g_hwndList = FindWindowEx(FindWindowEx(g_hwndParent, NULL, _T("#32770"), NULL), NULL, _T("SysListView32"), NULL);
 
   // g_exec is the complete command to run: It has the copy of this DLL turned
   // into an executable right now.
@@ -219,12 +270,17 @@
   popstring(pExec);
   if (my_strstr(pExec, _T("/TIMEOUT=")) == pExec) {
     TCHAR *szTimeout = pExec + 9;
-    g_to = my_atoi(szTimeout);
+    to = my_atoi(szTimeout);
     *pExec = 0;
     goto params;
   }
   if (!lstrcmpi(pExec, _T("/OEM"))) {
-    bOEM = TRUE;
+    bOEM = forceNarrowInput = TRUE;
+    *pExec = 0;
+    goto params;
+  }
+  if (!lstrcmpi(pExec, _T("/MBCS"))) {
+    forceNarrowInput = TRUE;
     *pExec = 0;
     goto params;
   }
@@ -242,155 +298,197 @@
   // Got all the params off the stack.
   
   {
-    STARTUPINFO si={sizeof(si),};
-    SECURITY_ATTRIBUTES sa={sizeof(sa),};
-    SECURITY_DESCRIPTOR sd={0,};
-    PROCESS_INFORMATION pi={0,};
+    STARTUPINFO si = { sizeof(si), };
+    SECURITY_ATTRIBUTES sa = { sizeof(sa), };
+    SECURITY_DESCRIPTOR sd = { 0, };
+    PROCESS_INFORMATION pi;
     const BOOL isNT = sizeof(void*) > 4 || (GetVersion() < 0x80000000);
-    HANDLE newstdout=0,read_stdout=0;
-    HANDLE newstdin=0,read_stdin=0;
-    DWORD dwRead = 1;
-    DWORD dwExit = 0;
-    DWORD dwWait = WAIT_TIMEOUT;
-    DWORD dwLastOutput;
-    static TCHAR szBuf[1024];
-#ifdef _UNICODE
-    static char ansiBuf[1024];
+    HANDLE newstdout = 0, read_stdout = 0;
+    HANDLE newstdin = 0, read_stdin = 0;
+    int utfSource = sizeof(TCHAR) > 1 && !forceNarrowInput ? -1 : FALSE, utfOutput = sizeof(TCHAR) > 1;
+    DWORD cbRead, dwLastOutput;
+    DWORD dwExit = 0, waitResult = WAIT_TIMEOUT;
+    static BYTE bufSrc[1024];
+    BYTE *pSrc;
+    SIZE_T cbSrcTot = sizeof(bufSrc), cbSrc = 0, cbSrcFree;
+    TCHAR *bufOutput = 0, *pNewAlloc, *pD;
+    SIZE_T cchAlloc, cbAlloc, cchFree;
+#ifndef _MSC_VER // Avoid GCC "may be used uninitialized in this function" warnings
+    pD = NULL;
+    cchAlloc = 0;
 #endif
-    HGLOBAL hUnusedBuf = NULL;
-    TCHAR *szUnusedBuf = 0;
 
-    if (log) {
-      hUnusedBuf = GlobalAlloc(GHND, log & 2 ? (g_stringsize*sizeof(TCHAR)) : sizeof(szBuf)*4); // Note: will not grow if (log & 2)
-      if (!hUnusedBuf) {
+    pi.hProcess = pi.hThread = NULL;
+    codepage = bOEM ? CP_OEMCP : CP_ACP;
+
+    if (!ignoreData) {
+      cbAlloc = stackMode ? (g_stringsize * sizeof(TCHAR)) : sizeof(bufSrc) * 4, cchAlloc = cbAlloc / sizeof(TCHAR);
+      pD = bufOutput = GlobalAlloc(GPTR, cbAlloc + sizeof(TCHAR)); // Include "hidden" space for a \0
+      if (!bufOutput) {
         lstrcpy(szRet, _T("error"));
         goto done;
       }
-      szUnusedBuf = (TCHAR *)GlobalLock(hUnusedBuf);
+      *bufOutput = _T('\0');
     }
 
-    sa.bInheritHandle = true;
+    sa.bInheritHandle = TRUE;
     sa.lpSecurityDescriptor = NULL;
     if (isNT) {
-      InitializeSecurityDescriptor(&sd,SECURITY_DESCRIPTOR_REVISION);
-      SetSecurityDescriptorDacl(&sd,true,NULL,false);
+      InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
+      SetSecurityDescriptorDacl(&sd, TRUE, NULL, FALSE);
       sa.lpSecurityDescriptor = &sd;
     }
 
-    if (!CreatePipe(&read_stdout,&newstdout,&sa,0)) {
+    if (!CreatePipe(&read_stdout, &newstdout, &sa, 0)) {
       lstrcpy(szRet, _T("error"));
       goto done;
     }
-    if (!CreatePipe(&read_stdin,&newstdin,&sa,0)) {
+    if (!CreatePipe(&read_stdin, &newstdin, &sa, 0)) {
       lstrcpy(szRet, _T("error"));
       goto done;
     }
 
-    GetStartupInfo(&si);
+    GetStartupInfo(&si); // Why?
     si.dwFlags = STARTF_USESTDHANDLES|STARTF_USESHOWWINDOW;
     si.wShowWindow = SW_HIDE;
     si.hStdInput = newstdin;
     si.hStdOutput = newstdout;
     si.hStdError = newstdout;
-    if (!CreateProcess(NULL,g_exec,NULL,NULL,TRUE,CREATE_NEW_CONSOLE,NULL,NULL,&si,&pi)) {
+    if (!CreateProcess(NULL, g_exec, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)) {
       lstrcpy(szRet, _T("error"));
       goto done;
     }
 
+    // Now I'm talking with an executable copy of myself.
     dwLastOutput = GetTickCount();
+    for (;;) {
+      TCHAR bufCh[2];
+waitForProcess:
+      waitResult = WaitForSingleObject(pi.hProcess, 0);
+      GetExitCodeProcess(pi.hProcess, &dwExit);
+readMore:
+      PeekNamedPipe(read_stdout, 0, 0, 0, &cbRead, NULL);
+      if (!cbRead) {
+        if (waitResult == WAIT_OBJECT_0) {
+          break; // No data in the pipe and process ended, we are done
+        }
+        if (to && GetTickCount() > dwLastOutput+to) {
+          TerminateProcess(pi.hProcess, -1);
+          lstrcpy(szRet, _T("timeout"));
+        }
+        else {
+          Sleep(LOOPTIMEOUT);
+        }
+        continue;
+      }
 
-    // Now I'm talking with an executable copy of myself.
-    while (dwWait != WAIT_OBJECT_0 || dwRead) {
-      PeekNamedPipe(read_stdout, 0, 0, 0, &dwRead, NULL);
-      if (dwRead) {
-        dwLastOutput = GetTickCount();
-#ifdef _UNICODE
-        ReadFile(read_stdout, ansiBuf, sizeof(ansiBuf)-1, &dwRead, NULL);
-        ansiBuf[dwRead] = 0;
-        WideConvertIfASCII(ansiBuf, dwRead, szBuf, sizeof(szBuf)/sizeof(szBuf[0]));
+      dwLastOutput = GetTickCount();
+      ReadFile(read_stdout, bufSrc + cbSrc, (DWORD) (cbSrcFree = cbSrcTot - cbSrc), &cbRead, NULL);
+      cbSrcFree -= cbRead, cbSrc = cbSrcTot - cbSrcFree;
+      pSrc = bufSrc;
+
+      if (utfSource < 0 && cbSrc) { // Simple UTF-16LE detection
+#ifdef UNICODE
+        utfSource = IsTextUnicode(pSrc, (UINT) (cbSrc & ~1), NULL) != FALSE;
 #else
-        ReadFile(read_stdout, szBuf, sizeof(szBuf)-1, &dwRead, NULL);
-        szBuf[dwRead] = '\0';
+        utfSource = (cbSrc >= 3 && pSrc[0] && !pSrc[1]) || (cbSrc > 4 && pSrc[2] && !pSrc[3]); // Lame latin-only test
+        utfSource |= (cbSrc > 3 && pSrc[0] == 0xFF && pSrc[1] == 0xFE && (pSrc[2] | pSrc[3])); // Lame BOM test
 #endif
-        if (log) {
-          if (log & 2) {
-            lstrcpyn(szUnusedBuf + lstrlen(szUnusedBuf), szBuf, g_stringsize - lstrlen(szUnusedBuf));
+      }
+
+      if (ignoreData) {
+        cbSrc = 0; // Overwrite the whole buffer every read
+        continue;
+      }
+
+      if (!cbRead) {
+        continue; // No new data, read more before trying to parse
+      }
+
+parseLines:
+      cchFree = cchAlloc - (pD - bufOutput);
+      for (;;) {
+        DWORD cbSrcChar = 1, cchDstChar, i;
+        *pD = _T('\0'); // Terminate output buffer because we can unexpectedly run out of data
+        if (!cbSrc) {
+          goto readMore;
+        }
+
+        if (utfSource) { // UTF-16LE --> ?:
+          if (cbSrc < 2) {
+            goto readMore;
+          }
+          if (utfOutput) { // UTF-16LE --> UTF-16LE:
+            bufCh[0] = ((TCHAR*)pSrc)[0], cbSrcChar = sizeof(WCHAR), cchDstChar = 1; // We only care about certain ASCII characters so we don't bother dealing with surrogate pairs.
+          }
+          else { // UTF-16LE --> DBCS
+            // TODO: This is tricky because we need the complete base character (or surrogate pair) and all the trailing combining characters for a grapheme in the buffer before we can call WideCharToMultiByte.
+            utfOutput = FALSE; // For now we just treat it as DBCS
+            continue;
           }
-          else {
-            TCHAR *p, *p2;
-            SIZE_T iReqLen = lstrlen(szBuf) + lstrlen(szUnusedBuf) + 1;
-            if (GlobalSize(hUnusedBuf) < iReqLen*sizeof(TCHAR)) {
-              GlobalUnlock(hUnusedBuf);
-              hUnusedBuf = GlobalReAlloc(hUnusedBuf, iReqLen*sizeof(TCHAR)+sizeof(szBuf), GHND);
-              if (!hUnusedBuf) {
-                lstrcpy(szRet, _T("error"));
-                break;
-              }
-              szUnusedBuf = (TCHAR *)GlobalLock(hUnusedBuf);
+        }
+        else { // DBCS --> ?:
+          if (utfOutput) { // DBCS --> UTF-16LE:
+            BOOL isMb = IsDBCSLeadByteEx(codepage, ((CHAR*)pSrc)[0]);
+            if (isMb && cbSrc < ++cbSrcChar) {
+              goto readMore;
             }
-            p = szUnusedBuf; // get the old left overs
-            lstrcat(p, szBuf);
-            while ((p = my_strstr(p, _T("\t")))) {
-              if ((int)(p - szUnusedBuf) > (int)(GlobalSize(hUnusedBuf)/sizeof(TCHAR) - TAB_REPLACE_SIZE - 1))
-              {
-                *p++ = _T(' ');
-              }
+            cchDstChar = MultiByteToWideChar(codepage, 0, (CHAR*)pSrc, cbSrcChar, (WCHAR*) bufCh, 2);
+          }
+          else { // DBCS --> DBCS:
+            bufCh[0] = ((CHAR*)pSrc)[0], cchDstChar = 1; // Note: OEM codepage will be converted by LogMessage
+          }
+        }
+
+        if (bufCh[0] == _T('\t') && tabExpandLength) { // Expand tab to spaces?
+          if (cchFree < tabExpandLength) {
+            goto resizeOutputBuffer;
+          }
+          lstrcpy(pD, TAB_REPLACE);
+          pD += tabExpandLength, cchFree -= tabExpandLength;
+        }
+        else if (bufCh[0] == _T('\r') && logMode) {
+          // Eating it
+        }
+        else if (bufCh[0] == _T('\n') && logMode) {
+          LogMessage(bufOutput, bOEM); // Output has already been \0 terminated
+          *(pD = bufOutput) = _T('\0'), cchFree = cchAlloc;
+        }
+        else {
+          if (cchFree < cchDstChar) {
+            SIZE_T cchOrgOffset;
+resizeOutputBuffer:
+            if (stackMode) {
+              ignoreData = TRUE; // Buffer was already maximum for the NSIS stack, we cannot handle more data
+              if (utfOutput) 
+                TruncateStringUTF16LE((LPWSTR) bufOutput, pD - bufOutput, (LPCWSTR) bufCh, cchDstChar);
               else
-              {
-                int len = lstrlen(p);
-                TCHAR *c_out=(TCHAR*)p+TAB_REPLACE_SIZE+len;
-                TCHAR *c_in=(TCHAR *)p+len;
-                while (len-- > 0) {
-                  *c_out--=*c_in--;
-                }
-
-                lstrcpy(p, TAB_REPLACE);
-                p += TAB_REPLACE_SIZE;
-                *p = _T(' ');
-              }
-            }
-            
-            p = szUnusedBuf; // get the old left overs
-            for (p2 = p; *p2;) {
-              if (*p2 == _T('\r')) {
-                *p2++ = 0;
-                continue;
-              }
-              if (*p2 == _T('\n')) {
-                *p2 = 0;
-                while (!*p && p != p2) p++;
-                LogMessage(p, bOEM);
-                p = ++p2;
-                continue;
-              }
-              p2 = CharNext(p2);
+                TruncateStringMB(codepage, (LPSTR) bufOutput, pD - bufOutput, bufCh[0]);
+              goto waitForProcess;
             }
-            
-            // If data was taken out from the unused buffer, move p contents to the start of szUnusedBuf
-            if (p != szUnusedBuf) {
-              TCHAR *p2 = szUnusedBuf;
-              while (*p) *p2++ = *p++;
-              *p2 = 0;
+            cchAlloc += 1024, cbAlloc = cchAlloc / sizeof(TCHAR);
+            pNewAlloc = GlobalReAlloc(bufOutput, cbAlloc + sizeof(TCHAR),GPTR|GMEM_MOVEABLE); // Include "hidden" space for a \0
+            if (!pNewAlloc) {
+              lstrcpy(szRet, _T("error"));
+              ignoreData = TRUE;
+              goto waitForProcess;
             }
+            cchOrgOffset = pD - bufOutput;
+            *(pD = (bufOutput = pNewAlloc) + cchOrgOffset) = _T('\0');
+            goto parseLines;
+          }
+          for (i = 0; i < cchDstChar; ++i) {
+            *pD++ = bufCh[i], --cchFree;
           }
         }
+        pSrc += cbSrcChar, cbSrc -= cbSrcChar;
       }
-      else {
-        if (g_to && GetTickCount() > dwLastOutput+g_to) {
-          TerminateProcess(pi.hProcess, -1);
-          lstrcpy(szRet, _T("timeout"));
-        }
-        else Sleep(LOOPTIMEOUT);
-      }
-
-      dwWait = WaitForSingleObject(pi.hProcess, 0);
-      GetExitCodeProcess(pi.hProcess, &dwExit);
-      PeekNamedPipe(read_stdout, 0, 0, 0, &dwRead, NULL);
     }
+
 done:
-    if (log & 2) pushstring(szUnusedBuf);
-    if (log & 1 && *szUnusedBuf) LogMessage(szUnusedBuf, bOEM);
-    if ( dwExit == STATUS_ILLEGAL_INSTRUCTION )
+    if (stackMode) pushstring(bufOutput);
+    if (logMode && *bufOutput) LogMessage(bufOutput,bOEM); // Write remaining output
+    if (dwExit == STATUS_ILLEGAL_INSTRUCTION)
       lstrcpy(szRet, _T("error"));
     if (!szRet[0]) wsprintf(szRet,_T("%d"),dwExit);
     pushstring(szRet);
@@ -402,33 +500,14 @@
     CloseHandle(read_stdin);
     if (pExec-2 >= g_exec)
       *(pExec-2) = _T('\0'); // skip space and quote
-    if (executor) DeleteFile(executor);
+    if (executor) 
+      DeleteFile(executor);
     GlobalFree(g_exec);
-    if (log) {
-      GlobalUnlock(hUnusedBuf);
-      GlobalFree(hUnusedBuf);
-    }
+    if (bufOutput)
+      GlobalFree(bufOutput);
   }
 }
 
-// Tim Kosse's LogMessage
-void LogMessage(const TCHAR *pStr, BOOL bOEM) {
-  LVITEM item={0};
-  int nItemCount;
-  if (!g_hwndList) return;
-  //if (!lstrlen(pStr)) return;
-#ifndef _UNICODE
-  if (bOEM == TRUE) OemToCharBuff(pStr, (char*)pStr, lstrlen(pStr));
-#endif
-  nItemCount=(int) SendMessage(g_hwndList, LVM_GETITEMCOUNT, 0, 0);
-  item.mask=LVIF_TEXT;
-  item.pszText=(TCHAR *)pStr;
-  item.cchTextMax=0;
-  item.iItem=nItemCount;
-  ListView_InsertItem(g_hwndList, &item);
-  ListView_EnsureVisible(g_hwndList, item.iItem, 0);
-}
-
 TCHAR *my_strstr(TCHAR *a, TCHAR *b)
 {
   int l = lstrlen(b);
  • Maybe we can try replacing the nsExec.dll file in your NSIS installation in the conda environment. You can follow the same instructions as in the comment above, but this time only replace the nsExec.dll from the nsis-3.*.zip files in this page. If we manage to find a later version than 3.01 that works, we might narrow down which change is causing the error.

@jaimergp
Copy link
Contributor

jaimergp commented Sep 5, 2022

Good news, I could reproduce the error, found the cause and provided a fix in #563!

If you need it fixed urgently, the patch can be as simple as changing this line:

nsExec::ExecToLog $2

To

-        nsExec::ExecToLog $2
+        nsExec::Exec $2

This will reduce the verbosity of the Details panel, though. The PR introduces a method to minimize this side effect as much as possible.

@beeankha beeankha added source::community catch-all for issues filed by community members mitigated a workaround has been identified labels Sep 6, 2022
@jaimergp jaimergp unpinned this issue Sep 7, 2022
@larsoner larsoner moved this to Done in 🧭 Planning Sep 8, 2022
@scottwsides
Copy link

Thanks!!!

@JinYongWu
Copy link
Author

What is the earliest version of Constructor which has this fix?

@jaimergp
Copy link
Contributor

jaimergp commented May 8, 2023

3.4.0, but realistically speaking, 3.4.1 (hotfix for 3.4.0).

@github-actions github-actions bot added the locked [bot] locked due to inactivity label May 8, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 8, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
locked [bot] locked due to inactivity mitigated a workaround has been identified source::community catch-all for issues filed by community members type::bug describes erroneous operation, use severity::* to classify the type
Projects
Archived in project
Development

Successfully merging a pull request may close this issue.

5 participants