When running interactive measurement utilities (such as chartread, spotread, dispcal, etc.) as child processes from frontend applications on Windows (e.g. Tauri, Node.js, Python subprocesses) with redirected standard input, holding or pressing the instrument trigger button does not initiate measurement and the measurement lamp never illuminates.
While calibration succeeds, the process hangs in the polling loop because ReadFile() deadlocks on the anonymous standard input pipe during non-blocking UI callback checks.
Root Cause Analysis
Failure of SetNamedPipeHandleState on Anonymous Pipes:
In numlib/numsup.c (check_if_not_interactive()), ArgyllCMS attempts to configure standard input for non-blocking I/O when redirected:
Under Windows Win32 API, SetNamedPipeHandleState()fails on anonymous pipes (e.g. pipes created by Win32 CreatePipe() or standard library subprocess spawn implementations like Rust Stdio::piped()). The anonymous pipe remains in synchronous blocking mode.
Blocking Read in Non-Blocking Character Polling:
In spectro/conv.c (con_char(int wait)), when not_interactive == 1 and stdin_type == FILE_TYPE_PIPE:
When wait == 0 (non-blocking poll via poll_con_char()), ReadFile() is called expecting it to return immediately if no data is available. Because the anonymous pipe is blocking, ReadFile() blocks indefinitely waiting for stdin input from the parent process.
Deadlock in Instrument Switch Loop:
During strip/spot measurement, i1pro_imp_measure() periodically invokes p->uicallback() to check for user abort/trigger commands. This invokes def_uicallback() (spectro/instappsup.c) -> poll_con_char() -> con_char(0).
Because con_char(0) hangs on ReadFile(), the measurement thread never resumes to poll USB endpoint 0x84 for instrument switch events, completely preventing hardware trigger detection and lamp activation.
Deterministic Remediation Plan
1. File Modification: spectro/conv.c
Use PeekNamedPipe() to query available bytes before calling ReadFile() on pipe handles. PeekNamedPipe() is fully supported on both anonymous pipes and named pipes in Win32:
/* We assume pipe has been set to NOWAIT mode. */}elseif(stdin_type==FILE_TYPE_PIPE){inti,bib;DWORDbytes_avail=0;/* Check available bytes in pipe without blocking */if(!PeekNamedPipe(stdinh,NULL,0,NULL,&bytes_avail,NULL)||bytes_avail==0){if(!wait){return0;}}for(bib=0;bib<10;){if(!PeekNamedPipe(stdinh,NULL,0,NULL,&bytes_avail,NULL)||bytes_avail==0){if(!wait)break;}if(!ReadFile(stdinh,buf+bib,10-bib,&bread,NULL)||bread==0){if(!wait)break;}bib+=bread;for(i=0;i<bib;i++){if(buf[i]=='\n'||buf[i]=='\r'||buf[i]==0x3){break;}}if(i<bib){break;/* Found '\n' */}Sleep(100);/* Wait for a line ending in '\n' */}rv=buf[0];
Create Fix Branch:
Create a branch named fix/win32-pipe-peek-named-pipe based on development.
git checkout development
git pull origin development
git checkout -b fix/win32-pipe-peek-named-pipe
Apply Code Changes:
Update spectro/conv.c with the PeekNamedPipe non-blocking check.
Commit and Push:
git add spectro/conv.c
git commit -m "fix(spectro): use PeekNamedPipe to prevent blocking ReadFile on anonymous stdin pipes"
git push -u origin fix/win32-pipe-peek-named-pipe
Pull Request into development & Close Issue:
Open PR from fix/win32-pipe-peek-named-pipe into development.
Merge PR into development.
Close this issue.
Pull Request into main:
Open PR from development into main.
Merge PR into main.
Create Release v3.5.0-ICCery.1.2:
Tag and publish release v3.5.0-ICCery.1.2 targeting main.
Ensure CI workflow packages Windows binary archive with all drivers, docs, and verified executables.
### Bug Summary
When running interactive measurement utilities (such as `chartread`, `spotread`, `dispcal`, etc.) as child processes from frontend applications on Windows (e.g. Tauri, Node.js, Python subprocesses) with redirected standard input, holding or pressing the instrument trigger button does not initiate measurement and the measurement lamp never illuminates.
While calibration succeeds, the process hangs in the polling loop because `ReadFile()` deadlocks on the anonymous standard input pipe during non-blocking UI callback checks.
---
### Root Cause Analysis
1. **Failure of `SetNamedPipeHandleState` on Anonymous Pipes**:
In `numlib/numsup.c` (`check_if_not_interactive()`), ArgyllCMS attempts to configure standard input for non-blocking I/O when redirected:
```c
if ((stdinh = GetStdHandle(STD_INPUT_HANDLE)) != INVALID_HANDLE_VALUE) {
stdin_type = GetFileType(stdinh);
if (stdin_type == FILE_TYPE_PIPE) {
DWORD mode = PIPE_READMODE_BYTE | PIPE_NOWAIT;
SetNamedPipeHandleState(stdinh, &mode, NULL, NULL);
}
}
```
Under Windows Win32 API, `SetNamedPipeHandleState()` **fails on anonymous pipes** (e.g. pipes created by Win32 `CreatePipe()` or standard library subprocess spawn implementations like Rust `Stdio::piped()`). The anonymous pipe remains in synchronous **blocking** mode.
2. **Blocking Read in Non-Blocking Character Polling**:
In `spectro/conv.c` (`con_char(int wait)`), when `not_interactive == 1` and `stdin_type == FILE_TYPE_PIPE`:
```c
} else if (stdin_type == FILE_TYPE_PIPE) {
int i, bib;
for (bib = 0; bib < 10;) {
if ((!ReadFile(stdinh, buf + bib, 10 - bib, &bread, NULL) || bread == 0)
&& !wait) {
break;
}
bib += bread;
...
}
}
```
When `wait == 0` (non-blocking poll via `poll_con_char()`), `ReadFile()` is called expecting it to return immediately if no data is available. Because the anonymous pipe is blocking, **`ReadFile()` blocks indefinitely waiting for stdin input from the parent process**.
3. **Deadlock in Instrument Switch Loop**:
During strip/spot measurement, `i1pro_imp_measure()` periodically invokes `p->uicallback()` to check for user abort/trigger commands. This invokes `def_uicallback()` (`spectro/instappsup.c`) -> `poll_con_char()` -> `con_char(0)`.
Because `con_char(0)` hangs on `ReadFile()`, the measurement thread never resumes to poll USB endpoint `0x84` for instrument switch events, completely preventing hardware trigger detection and lamp activation.
---
### Deterministic Remediation Plan
#### 1. File Modification: `spectro/conv.c`
Use `PeekNamedPipe()` to query available bytes before calling `ReadFile()` on pipe handles. `PeekNamedPipe()` is fully supported on both anonymous pipes and named pipes in Win32:
```c
/* We assume pipe has been set to NOWAIT mode. */
} else if (stdin_type == FILE_TYPE_PIPE) {
int i, bib;
DWORD bytes_avail = 0;
/* Check available bytes in pipe without blocking */
if (!PeekNamedPipe(stdinh, NULL, 0, NULL, &bytes_avail, NULL) || bytes_avail == 0) {
if (!wait) {
return 0;
}
}
for (bib = 0; bib < 10;) {
if (!PeekNamedPipe(stdinh, NULL, 0, NULL, &bytes_avail, NULL) || bytes_avail == 0) {
if (!wait) break;
}
if (!ReadFile(stdinh, buf + bib, 10 - bib, &bread, NULL) || bread == 0) {
if (!wait) break;
}
bib += bread;
for (i = 0; i < bib; i++) {
if (buf[i] == '\n' || buf[i] == '\r' || buf[i] == 0x3) {
break;
}
}
if (i < bib) {
break; /* Found '\n' */
}
Sleep(100); /* Wait for a line ending in '\n' */
}
rv = buf[0];
```
---
### Step-by-Step Release Workflow Instructions (`v3.5.0-ICCery.1.2`)
1. **Create Fix Branch**:
Create a branch named `fix/win32-pipe-peek-named-pipe` based on `development`.
```bash
git checkout development
git pull origin development
git checkout -b fix/win32-pipe-peek-named-pipe
```
2. **Apply Code Changes**:
Update `spectro/conv.c` with the `PeekNamedPipe` non-blocking check.
3. **Commit and Push**:
```bash
git add spectro/conv.c
git commit -m "fix(spectro): use PeekNamedPipe to prevent blocking ReadFile on anonymous stdin pipes"
git push -u origin fix/win32-pipe-peek-named-pipe
```
4. **Pull Request into `development` & Close Issue**:
- Open PR from `fix/win32-pipe-peek-named-pipe` into `development`.
- Merge PR into `development`.
- Close this issue.
5. **Pull Request into `main`**:
- Open PR from `development` into `main`.
- Merge PR into `main`.
6. **Create Release `v3.5.0-ICCery.1.2`**:
- Tag and publish release `v3.5.0-ICCery.1.2` targeting `main`.
- Ensure CI workflow packages Windows binary archive with all drivers, docs, and verified executables.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Bug Summary
When running interactive measurement utilities (such as
chartread,spotread,dispcal, etc.) as child processes from frontend applications on Windows (e.g. Tauri, Node.js, Python subprocesses) with redirected standard input, holding or pressing the instrument trigger button does not initiate measurement and the measurement lamp never illuminates.While calibration succeeds, the process hangs in the polling loop because
ReadFile()deadlocks on the anonymous standard input pipe during non-blocking UI callback checks.Root Cause Analysis
Failure of
SetNamedPipeHandleStateon Anonymous Pipes:In
numlib/numsup.c(check_if_not_interactive()), ArgyllCMS attempts to configure standard input for non-blocking I/O when redirected:Under Windows Win32 API,
SetNamedPipeHandleState()fails on anonymous pipes (e.g. pipes created by Win32CreatePipe()or standard library subprocess spawn implementations like RustStdio::piped()). The anonymous pipe remains in synchronous blocking mode.Blocking Read in Non-Blocking Character Polling:
In
spectro/conv.c(con_char(int wait)), whennot_interactive == 1andstdin_type == FILE_TYPE_PIPE:When
wait == 0(non-blocking poll viapoll_con_char()),ReadFile()is called expecting it to return immediately if no data is available. Because the anonymous pipe is blocking,ReadFile()blocks indefinitely waiting for stdin input from the parent process.Deadlock in Instrument Switch Loop:
During strip/spot measurement,
i1pro_imp_measure()periodically invokesp->uicallback()to check for user abort/trigger commands. This invokesdef_uicallback()(spectro/instappsup.c) ->poll_con_char()->con_char(0).Because
con_char(0)hangs onReadFile(), the measurement thread never resumes to poll USB endpoint0x84for instrument switch events, completely preventing hardware trigger detection and lamp activation.Deterministic Remediation Plan
1. File Modification:
spectro/conv.cUse
PeekNamedPipe()to query available bytes before callingReadFile()on pipe handles.PeekNamedPipe()is fully supported on both anonymous pipes and named pipes in Win32:Step-by-Step Release Workflow Instructions (
v3.5.0-ICCery.1.2)Create Fix Branch:
Create a branch named
fix/win32-pipe-peek-named-pipebased ondevelopment.Apply Code Changes:
Update
spectro/conv.cwith thePeekNamedPipenon-blocking check.Commit and Push:
Pull Request into
development& Close Issue:fix/win32-pipe-peek-named-pipeintodevelopment.development.Pull Request into
main:developmentintomain.main.Create Release
v3.5.0-ICCery.1.2:v3.5.0-ICCery.1.2targetingmain.