How to Detect and Handle Scripts Blocked on Stdin Input in Linux Shell Programming


2 views

When automating shell scripts, a common pain point occurs when child processes get stuck waiting for standard input (stdin). This creates silent failures where:

  • Scripts appear to hang without output
  • No clear indication of why execution paused
  • Automation pipelines break unpredictably

Consider this blocking script (demo.sh):

#!/bin/bash
echo "About to read input..."
read -p "Enter value: " 
echo "Continuing execution..."

When piped into another command (failifwaitingonstdin in the original question), we need to detect the read operation's blocking state.

Method 1: Process State Monitoring

Linux processes show specific states in /proc:

grep -q "State:\tS (sleeping)" /proc/$PID/status && 
echo "Process blocked on I/O"

Method 2: Non-blocking I/O with Timeout

Using expect with timeout:

#!/usr/bin/expect
set timeout 5
spawn ./demo.sh
expect {
    "Enter value: " { exit 1 }
    timeout { exit 0 }
}

Method 3: LD_PRELOAD Interception

For advanced users, intercept read() calls:

// stdin_watchdog.c
#define _GNU_SOURCE
#include 
#include 

ssize_t (*original_read)(int, void*, size_t);

ssize_t read(int fd, void *buf, size_t count) {
    if(fd == STDIN_FILENO) {
        fprintf(stderr, "Blocking stdin read detected!\n");
        _exit(EXIT_FAILURE);
    }
    return original_read(fd, buf, count);
}

Compile with: gcc -shared -fPIC -o stdin_watchdog.so stdin_watchdog.c -ldl

For production systems, combine multiple approaches:

#!/bin/bash
TIMEOUT=30

# Start process in background
./long_running_script.sh &
PID=$!

# Monitoring loop
while kill -0 $PID 2>/dev/null; do
    if grep -q "State:\tS" /proc/$PID/status; then
        if lsof -p $PID | grep -q "0u.*CHR.*1,3"; then
            echo "STDIN BLOCK DETECTED" >&2
            kill $PID
            exit 1
        fi
    fi
    sleep 1
    TIMEOUT=$((TIMEOUT-1))
    [ $TIMEOUT -le 0 ] && break
done

For common cases, consider:

  • yes | ./script.sh - Feed continuous 'y' responses
  • ./script.sh < /dev/null - Explicit empty input
  • stdbuf -i0 -o0 -e0 ./script.sh - Disable I/O buffering

Essential Linux tools for analysis:

strace -e trace=read -p $PID  # System call tracing
lsof -p $PID                 # Open file descriptors
gdb -p $PID                  # Live process inspection

When automating shell scripts, we often encounter situations where a process appears hung but is actually blocked waiting for stdin input. This creates a significant challenge in automation pipelines where we need to distinguish between genuine processing time and input waiting states.

In Unix-like systems, we can inspect process states through several methods:

# Check process state
ps -o state= -p [PID]

# Possible states:
# S - Sleeping (interruptible)
# D - Uninterruptible sleep
# R - Running
# T - Stopped
# Z - Zombie

Here are three effective approaches to detect stdin blocking:

Method 1: Using lsof

#!/bin/bash
./demo &
PID=$!

sleep 0.1 # Allow process to start
if lsof -p $PID | grep -q '0u.*CHR'; then
    echo "Process is waiting on stdin" >&2
    kill $PID
    exit 1
fi

Method 2: Expect-based Solution

#!/usr/bin/expect -f
spawn ./demo
expect {
    timeout { exit 1 }
    eof { exit 0 }
}

Method 3: Non-blocking Pipe

#!/bin/bash
mkfifo mypipe
(./demo < mypipe &)
PID=$!

# Try writing to the pipe with timeout
if ! echo "" > mypipe & sleep 0.1; then
    kill $PID
    rm mypipe
    exit 1
fi

For more sophisticated scenarios, consider these approaches:

Using strace:

strace -e trace=read -p [PID] 2>&1 | grep '^read(0,'

PTY-based Solution:

#!/bin/bash
socat EXEC:./demo,pty,ctty,setsid STDIO

When implementing these solutions in production:

  • Use minimal sleep intervals (0.1s is usually sufficient)
  • Clean up any temporary files or pipes
  • Handle multiple concurrent processes carefully
  • Consider using process groups for clean termination

Here's how to integrate this check into a Jenkins pipeline:

pipeline {
    agent any
    stages {
        stage('Run Test') {
            steps {
                script {
                    def status = sh(
                        script: 'timeout 5s ./check_stdin.sh || true',
                        returnStatus: true
                    )
                    if (status == 124) {
                        error("Process hung waiting for input")
                    }
                }
            }
        }
    }
}