2025, Oct 31 12:03
stdin पर टाइमआउट सही तरह से कैसे करें: Python में select और रॉ मोड
ब्लॉकिंग I/O में stdin टाइमआउट क्यों नहीं होता और Unix पर इसे सही तरह से कैसे करें? Python में select के साथ रॉ मोड, सुरक्षित टर्मिनल हैंडलिंग और उदाहरण कोड समझें.
टाइमआउट के साथ stdin से पढ़ना सुनने में आसान लगता है, लेकिन जैसे ही ब्लॉकिंग I/O और टर्मिनल मोड सामने आते हैं, मामला बदल जाता है। एक आम गलती यह मान लेना है कि read कॉल का आर्ग्युमेंट सेकंड में टाइमआउट है, जबकि असल में वह पढ़े जाने वाले अधिकतम बाइट्स की संख्या होता है। नतीजा—स्क्रिप्ट अटकी हुई दिखती है और कभी टाइमआउट नहीं होता। आइए समझते हैं कि यह क्यों होता है और Unix-जैसी प्रणालियों पर इसे सही तरीके से कैसे किया जाए।
समस्या को पुन: उत्पन्न करना
नीचे दिया गया स्निपेट दिखने में 5-सेकंड के टाइमआउट के साथ उपयोगकर्ता इनपुट पढ़ने की कोशिश करता है। इसके बजाय, यह इनपुट आने तक ब्लॉक रहता है और जब उपयोगकर्ता चुप रहता है तो कभी None वापस नहीं करता।
import os
import tty
import sys
import time
in_fd = sys.stdin.fileno()
tty.setraw(in_fd)
print("Enter some text:", end=' ', flush=True)
start_ts = time.time()
buf = os.read(in_fd, 5)
print("Time taken:", time.time() - start_ts, "d:", buf)
ऐसा व्यवहार क्यों होता है
समझने के लिए दो मुख्य बातें हैं। पहली, डिफ़ॉल्ट रूप से stdin एक ब्लॉकिंग फ़ाइल डिस्क्रिप्टर होता है। उस पर os.read कॉल करने से तब तक इंतज़ार होता है जब तक पढ़ने को डेटा न मिल जाए। दूसरी बात, os.read का सिग्नेचर: दूसरा आर्ग्युमेंट टाइमआउट नहीं, बल्कि पढ़े जाने वाले बाइट्स की अधिकतम संख्या है। 5 पास करने का मतलब है “पाँच बाइट तक पढ़ो”, न कि “पाँच सेकंड इंतज़ार करो।” इसलिए कोड इनपुट उपलब्ध होने तक प्रॉम्प्ट पर अनिश्चित समय तक रुका रहता है। रॉ मोड में टर्मिनल कीस्ट्रोक तुरंत देता है और लाइन-बफ़रिंग नहीं करता, इसलिए इनपुट अक्षर-दर-अक्षर दिखता है, लेकिन इससे read कॉल नॉन-ब्लॉकिंग या टाइम-अवेयर नहीं बनती।
फिर भी stdin एक प्रतीक्षा-योग्य ऑब्जेक्ट है, यानी आप वास्तविक पढ़ाई से पहले OS से पूछ सकते हैं कि डेटा तैयार है या नहीं। इसके लिए select के साथ टाइमआउट इस्तेमाल किया जाता है। दिए गए अंतराल में select तैयार होने का संकेत दे दे, तो आगे बढ़ें; वरना इसे टाइमआउट मानें।
समाधान: टाइमआउट के साथ select का इस्तेमाल करें और रॉ मोड को सुरक्षित तरीके से संभालें
नीचे दिया गया तरीका टर्मिनल को रॉ मोड में सेट करता है, उपयोगकर्ता को प्रॉम्प्ट दिखाता है, फिर select के जरिए निर्दिष्ट टाइमआउट तक इंतज़ार करता है। डेटा आते ही यह इंटरएक्टिव प्रॉम्प्ट से अपेक्षित कंट्रोल कैरेक्टर—जैसे बैकस्पेस, कैरेज रिटर्न और एस्केप सीक्वेन्स—को संभालता है। अगर टाइमआउट पहले खत्म हो जाए, तो None लौटाता है। सबसे अहम, बाहर निकलते समय यह टर्मिनल सेटिंग्स भी बहाल कर देता है।
from select import select
from sys import stdin
from tty import setraw
from termios import tcsetattr, TCSAFLUSH
from functools import partial
BK = "\b"
DELETE = "\x7f"
SIGINT = "\x03"
RET = "\r"
ESCAPE = "\x1b"
CSI = ESCAPE + "["
ERASE_LINE = CSI + "K"
REFRESH = RET + ERASE_LINE
ECHO = partial(print, end="", flush=True)
CSI_DIGITS = set("0123456789;?")
def is_ready(fd: int, timeout: float) -> bool:
    r, _, _ = select([fd], [], [], timeout)
    return bool(r)
def swallow_escape() -> None:
    if stdin.read(1) == "[":
        while stdin.read(1) in CSI_DIGITS:
            pass
def read_with_timeout(prompt: str = "", timeout: float = 0.0) -> str | None:
    if timeout <= 0.0:
        return input(prompt)
    ECHO(prompt)
    fd = stdin.fileno()
    prev_attrs = setraw(fd)
    buf = ""
    try:
        while True:
            if not is_ready(fd, timeout):
                return None
            ch = stdin.read(1)
            if ch in {RET, SIGINT}:
                break
            if ch == ESCAPE:
                swallow_escape()
                continue
            if ch in {BK, DELETE}:
                if buf:
                    buf = buf[:-1]
                    ECHO(f"{REFRESH}{prompt}{buf}")
            else:
                buf += ch
                ECHO(ch)
    finally:
        tcsetattr(fd, TCSAFLUSH, prev_attrs)
        print()
    return buf
if __name__ == "__main__":
    print(read_with_timeout("Enter something: ", 5))
यह केवल Unix-जैसे प्लेटफ़ॉर्म पर ही काम कर सकता है।
असल में हो क्या रहा है
stdin को रेडीनेस जाँचने के लिए select के साथ इस्तेमाल किया जा सकता है। यह कॉल टाइमआउट लेती है और तब लौटती है जब डिस्क्रिप्टर रीडेबल हो जाए या टाइमआउट बीत जाए। इसे रॉ मोड के साथ मिलाकर आपको बिना कैनॉनिकल लाइन एडिटिंग के कीस्ट्रोक तुरंत मिलते हैं, और “इनपुट पूरा” किसे मानना है, इस पर सटीक नियंत्रण बना रहता है। ऊपर का कोड इनपुट को फाइनल करने के लिए कैरेज रिटर्न स्वीकार करता है, Ctrl-C का सम्मान करता है, एस्केप सीक्वेन्स नज़रअंदाज़ करता है, और बैकस्पेस या डिलीट संभालते समय लाइन को फिर से ड्रॉ कर प्रॉम्प्ट को साफ़ रखता है।
अगर आप सोच रहे हैं कि शुरुआती कोशिश में इनपुट एक-एक अक्षर करके क्यों स्वीकार होता दिखा, तो वह रॉ मोड का असर है: कैरेक्टर तुरंत आपके प्रोसेस को उपलब्ध करा दिए जाते हैं। इससे यह तथ्य नहीं बदलता कि एक ब्लॉकिंग read तब तक इंतज़ार करेगा जब तक कम-से-कम एक बाइट न मिल जाए, और आपके द्वारा दिया गया “5” केवल लौटाए जाने वाले बाइट्स की संख्या सीमित करता है, कॉल कितनी देर इंतज़ार करेगी यह नहीं।
एक और व्यावहारिक पैटर्न भी है: एक अलग थ्रेड में समर्पित रीडर चलाना जो कीबोर्ड से ब्लॉकिंग read करे, परिणाम को एक क्यू में डाले, और मुख्य थ्रेड उस क्यू पर टाइमआउट के साथ get कॉल करे। इससे मुख्य प्रवाह ब्लॉक हुए बिना टाइमआउट का लाभ मिल जाता है।
यह क्यों मायने रखता है
इंटरैक्टिव CLI टूल, TUI ऐप्स और REPL-जैसी यूटिलिटियाँ अक्सर यूज़र इनपुट को टाइमर, प्रोग्रेस फ़ीडबैक और अन्य असिंक्रोनस घटनाओं के साथ मिलाती हैं। ब्लॉकिंग I/O कैसे काम करता है, इसे गलत समझने से प्रोग्राम अनुत्तरदायी हो जाते हैं और UX नाज़ुक पड़ जाती है। stdin पर टाइमआउट के साथ select का उपयोग आपको पूर्वानुमेय व्यवहार देता है और उत्तरदायी टर्मिनल इंटरैक्शन बनाने के लिए ज़रूरी नियंत्रण प्रदान करता है।
मुख्य बातें
जब टर्मिनल इनपुट पर टाइमआउट चाहिए, तो os.read से ऐसी उम्मीदें न बाँधें जो वह पूरी ही नहीं करता। stdin को एक प्रतीक्षा-योग्य स्रोत मानें, टाइमआउट के साथ select कॉल करें, और तभी read करें। तुरंत कीस्ट्रोक चाहिए हों तो टर्मिनल को रॉ मोड में स्विच करना और बाद में उसे बहाल करना याद रखें। अगर आपके डिजाइन में टर्मिनल मोड सीधे छेड़ना पसंद नहीं, तो ब्लॉकिंग read को क्यू के साथ एक वर्कर थ्रेड में भेज देना भी एक विकल्प है।
यह लेख StackOverflow के एक प्रश्न (लेखक: DeepThought42) और Ramrab के उत्तर पर आधारित है।