मैंने हाल ही में इस प्रश्न का उत्तर प्रदान किया: C# - Realtime console output redirection।एसिंक को कार्यान्वित करना "स्ट्रीम से वर्तमान में उपलब्ध सभी डेटा पढ़ें" ऑपरेशन
अक्सर होता है, सामानों को समझाते हुए (यहां "सामान" यह था कि मैंने एक जैसी समस्या का सामना कैसे किया) आपको अधिक समझ और/या, यहां मामला है, "ओह" क्षण। मुझे एहसास हुआ कि मेरा समाधान, लागू होने के रूप में, एक बग है। बग का थोड़ा व्यावहारिक महत्व नहीं है, लेकिन डेवलपर के रूप में मेरे लिए यह बहुत बड़ा महत्व है: मैं आसानी से यह जानना आराम नहीं कर सकता कि मेरे कोड में उड़ने की क्षमता है।
बग स्क्वैशिंग इस प्रश्न का उद्देश्य है। मैं लंबे परिचय के लिए क्षमा चाहता हूं, तो चलो गंदे हो जाएं।
मैं एक कक्षा बनाना चाहता था जो मुझे कंसोल के मानक आउटपुट Stream
से इनपुट प्राप्त करने की अनुमति देता है। कंसोल आउटपुट स्ट्रीम FileStream
प्रकार के हैं; अगर आवश्यक हो तो कार्यान्वयन उस पर डाल सकता है। लीवरेज के लिए पहले से जुड़े StreamReader
भी मौजूद हैं।
मेरी वांछित कार्यक्षमता प्राप्त करने के लिए मुझे इस वर्ग में केवल एक चीज लागू करने की आवश्यकता है: एक async "इस पल उपलब्ध सभी डेटा को पढ़ें" ऑपरेशन। स्ट्रीम के अंत में पढ़ना व्यवहार्य नहीं है क्योंकि स्ट्रीम तब तक समाप्त नहीं होगी जब तक प्रक्रिया कंसोल आउटपुट हैंडल को बंद न करे, और ऐसा नहीं होगा क्योंकि यह इंटरैक्टिव है और जारी रखने से पहले इनपुट की अपेक्षा करता है।
मैं घटना-आधारित अधिसूचना को लागू करने के लिए उस hypothetical async ऑपरेशन का उपयोग करूँगा, जो मेरे कॉलर्स के लिए अधिक सुविधाजनक होगा।
वर्ग के सार्वजनिक इंटरफ़ेस यह है:
public class ConsoleAutomator {
public event EventHandler<ConsoleOutputReadEventArgs> StandardOutputRead;
public void StartSendingEvents();
public void StopSendingEvents();
}
StartSendingEvents
और StopSendingEvents
वे क्या विज्ञापित करते हैं; इस चर्चा के प्रयोजनों के लिए, हम मान सकते हैं कि घटनाओं को हमेशा सामान्यता के नुकसान के बिना भेजा जा रहा है।
वर्ग इन दो क्षेत्रों को आंतरिक रूप से उपयोग करता है:
protected readonly StringBuilder inputAccumulator = new StringBuilder();
protected readonly byte[] buffer = new byte[256];
वर्ग की कार्यक्षमता नीचे दिए गए तरीकों में कार्यान्वित किया जाता। गेंद रोलिंग मिल के लिए:
public void StartSendingEvents();
{
this.stopAutomation = false;
this.BeginReadAsync();
}
भी एक गाड़ी वापसी चार की आवश्यकता के बिना रोके बिना Stream
से बाहर डेटा पढ़ने के लिए, और, BeginRead
कहा जाता है:
protected void BeginReadAsync()
{
if (!this.stopAutomation) {
this.StandardOutput.BaseStream.BeginRead(
this.buffer, 0, this.buffer.Length, this.ReadHappened, null);
}
}
चुनौतीपूर्ण हिस्सा:
BeginRead
को एक बफर का उपयोग करने की आवश्यकता है। इसका मतलब है कि स्ट्रीम से पढ़ने पर, यह संभव है कि पढ़ने के लिए उपलब्ध बाइट्स ("आने वाले खंड") बफर से बड़े होते हैं। याद रखें कि यहां लक्ष्य है जो सभी खंड पढ़ने के लिए है और प्रत्येक ग्राहक के लिए बिल्कुल एक बार ईवेंट ग्राहक पर कॉल करें।
इस अंत में, यदि बफर EndRead
के बाद भरा हुआ है, तो हम इसकी सामग्री तुरंत ग्राहकों को नहीं भेजते हैं बल्कि इसके बजाय उन्हें StringBuilder
पर जोड़ते हैं। StringBuilder
की सामग्री केवल तब भी भेजी जाती है जब स्ट्रीम से पढ़ने के लिए और कुछ नहीं होता है।
private void ReadHappened(IAsyncResult asyncResult)
{
var bytesRead = this.StandardOutput.BaseStream.EndRead(asyncResult);
if (bytesRead == 0) {
this.OnAutomationStopped();
return;
}
var input = this.StandardOutput.CurrentEncoding.GetString(
this.buffer, 0, bytesRead);
this.inputAccumulator.Append(input);
if (bytesRead < this.buffer.Length) {
this.OnInputRead(); // only send back if we 're sure we got it all
}
this.BeginReadAsync(); // continue "looping" with BeginRead
}
किसी भी पढ़ा जो बफर (इस स्थिति में हम जानते हैं कि कोई और अधिक डेटा पिछले पढ़ने आपरेशन के दौरान पढ़ा जाए थी) को भरने के लिए पर्याप्त नहीं है के बाद, सभी संचित डेटा ग्राहकों के लिए भेज दिया जाता है:
private void OnInputRead()
{
var handler = this.StandardOutputRead;
if (handler == null) {
return;
}
handler(this,
new ConsoleOutputReadEventArgs(this.inputAccumulator.ToString()));
this.inputAccumulator.Clear();
}
(मुझे पता है कि जब तक कोई ग्राहक नहीं होता है तब तक डेटा हमेशा जमा हो जाता है। यह एक जानबूझकर निर्णय है)।
अच्छा
यह योजना काम करता है लगभग पूरी तरह से: किसी भी धागे
- Async कार्यक्षमता (सिर्फ एक घटना की सदस्यता)
- पढ़ने के लिए प्रत्येक बार डेटा उपलब्ध होने के लिए कभी भी एक से अधिक ईवेंट
- बफर आकार
करने के लिए लगभग नास्तिक है बुरा
कि पिछले लगभग एक बहुत बड़ा एक है। गौर करें कि क्या होता है जब बफर के आकार के बराबर लंबाई के साथ आने वाला हिस्सा होता है। खंड को पढ़ा और buffered किया जाएगा, लेकिन घटना ट्रिगर नहीं किया जाएगा। इसके बाद BeginRead
का पालन किया जाएगा जो वर्तमान खंड से संबंधित अधिक डेटा को एक टुकड़े में वापस भेजने के लिए अपेक्षा करता है, लेकिन ... स्ट्रीम में कोई और डेटा नहीं होगा।
वास्तव में, जब तक डेटा को खंड में धारा में रखा जाता है, लंबाई के बराबर लंबाई के बराबर लंबाई के साथ, डेटा buffered किया जाएगा और घटना कभी ट्रिगर नहीं किया जाएगा।
इस परिदृश्य में अभ्यास में होने की संभावना बहुत कम हो सकती है, खासकर जब से हम बफर आकार के लिए कोई भी नंबर चुन सकते हैं, लेकिन समस्या वहां है।
समाधान?
दुर्भाग्य से, FileStream
और StreamReader
पर उपलब्ध तरीकों की जाँच के बाद, मैं कुछ भी नहीं करते हुए भी इजाजत दी async तरीकों उस पर इस्तेमाल किया जा करने के लिए मुझे धारा में झांक सकते हैं जिसकी मदद से पा सकते हैं।
"बफर भरने" स्थिति के बाद एक "समाधान" ManualResetEvent
पर थ्रेड प्रतीक्षा करना होगा। यदि घटना को थोड़ी सी अवधि में संकेत नहीं दिया जाता है (एसिंक कॉलबैक द्वारा), तो स्ट्रीम से अधिक डेटा आगामी नहीं होगा और अब तक जमा डेटा को ग्राहकों को भेजा जाना चाहिए। हालांकि, यह एक और धागे की आवश्यकता को प्रस्तुत करता है, थ्रेड सिंक्रनाइज़ेशन की आवश्यकता होती है, और यह सादा सुरुचिपूर्ण है।
BeginRead
के लिए टाइमआउट निर्दिष्ट करना भी पर्याप्त होगा (अब मेरे कोड में वापस कॉल करें और फिर मैं जांच कर सकता हूं कि डेटा वापस भेजा जाए या नहीं, अधिकांश समय ऐसा करने के लिए कुछ भी नहीं होगा, इसलिए मुझे उम्मीद है कि प्रदर्शन नगण्य होने के लिए मारा)। लेकिन ऐसा लगता है कि FileStream
में टाइमआउट समर्थित नहीं हैं।
चूंकि मुझे लगता है कि टाइमआउट के साथ एसिंक कॉल नंगे Win32 में एक विकल्प है, तो एक और दृष्टिकोण समस्या से बाहर नरक को पीएनवोक करना हो सकता है। लेकिन यह भी अवांछनीय है क्योंकि यह जटिलता पेश करेगा और कोड को दर्द होगा।
क्या समस्या के आसपास आने का एक शानदार तरीका है?
यह सब पढ़ने के लिए पर्याप्त रोगी होने के लिए धन्यवाद।
अद्यतन:
मैं निश्चित रूप से मेरी प्रारंभिक writeup में अच्छी तरह से परिदृश्य संवाद नहीं था। मैंने बाद में लेखन को थोड़ा सा संशोधित किया है, लेकिन अतिरिक्त सुनिश्चित करने के लिए:
प्रश्न यह है कि "इस पल उपलब्ध सभी डेटा को पढ़ें" ऑपरेशन को एसिंक को कैसे कार्यान्वित किया जाए, इस बारे में सवाल है।
उन लोगों के लिए मेरी माफ़ी, जिन्होंने मुझे अपना इरादा स्पष्ट किए बिना पढ़ने और जवाब देने का समय लिया।
मुझे विश्वास है कि धाराओं के बारे में आपकी धारणा गलत हो सकती है। "भाग" की कोई अवधारणा नहीं है। एक धारा बाइट्स का सिर्फ एक अनुक्रम है। प्रदत्त बफर को भरने के लिए रीड विधि की आवश्यकता नहीं है, इसे केवल कम से कम एक बाइट वापस करने की आवश्यकता है। उन हिस्सों को पुनर्निर्माण करने का कोई तरीका नहीं है जिसमें डेटा स्ट्रीम में लिखा गया था। – dtb
@ डीटीबी: मुझे पता है कि सिद्धांत में धाराओं के पास "भाग" के साथ बिल्कुल कुछ नहीं है। अभ्यास में, हालांकि, आप उम्मीद करते हैं कि "खंड" को अक्सर स्ट्रीम के माध्यम से वितरित किया जाएगा (उदाहरण के लिए टीसीपी के माध्यम से संचार करने वाली दो प्रक्रियाएं निश्चित आकार के "संदेश" का आदान-प्रदान कर सकती हैं)। इस परिदृश्य में भी एक खंड की अवधारणा बहुत महत्वपूर्ण है, क्योंकि मैं कंसोल एप्लिकेशन को स्वचालित करना चाहता हूं। तो यहां, खंड जो भी स्वचालित प्रक्रिया स्ट्रीम फ्लश के बीच अपने मानक आउटपुट को लिखते हैं। प्रक्रिया फ्लश के साथ स्ट्रीम में "संरचना" प्रदान करती है; मैं उस रिवर्स-इंजीनियर को नहीं करना चाहता हूं। – Jon
दाएं। लेकिन चूंकि किसी स्ट्रीम में भाग की कोई अवधारणा नहीं होती है, इसलिए स्ट्रीम पर संदेशों को भेजने वाले किसी भी एप्लिकेशन को इसके आकार को किसी तरह से संवाद करने की आवश्यकता होती है (उदाहरण के लिए, यह मानकर कि सभी संदेशों के समान, निश्चित आकार का आकार है; आकार के साथ संदेश को उपसर्ग करके; या संदेशों को किसी भी तरह से सीमित करके)। धाराओं में एक या कई हिस्सों में संदेशों को लिखा गया था या नहीं, इस बारे में जानकारी खो गई है। – dtb