事情是這樣子的…
前兩天同事跟我說有一個使用了Tika的程式在處理txt文字檔有時候不知道什麼原因會卡住死掉,問我可不可以改用CancellationToken的方式讓它超過時間就取消處理?
程式碼大概是這樣的…
public string Extract(Stream stream)
{
var Arg = $"-jar {TikaPath} -t --encoding=UTF-16 -";
//run Tika
var p = new Process();
p.StartInfo.FileName = JavaPath;
p.StartInfo.Arguments = Arg;
p.StartInfo.CreateNoWindow = true;
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardInput = true;
p.StartInfo.RedirectStandardOutput = true;
p.Start();
var stdin = p.StandardInput.BaseStream;
stream.CopyTo(stdin);
stdin.Close();
var output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
return output;
}
從別的地方讀取了檔案之後呼叫了這個方法,傳入了一個Stream,透過Stream.CopyTo的方式將傳入的內容複製到Tika的StandardInput,用ConsoleApp很簡單的弄了一個測試,逐步執行的時候會發現就卡在Stream.CopyTo這個地方,但是如果是pdf檔案就可以順利執行。
Stream.CopyTo有一個非同步的方法是CopyToAsync,稍微試了一下就算用了CancellationToken嘗試在超過時間要取消操作,實際上仍然是沒辦法中止卡在這個地方的情況。而且這樣根本也不知其所以然,到底為什麼卡住還是不知道原因。
OK,既然是卡在這個Stream複製的地方,而且不同的檔案(txt vs pdf)在相同的程式碼是不同的結果,pdf可以正常執行,txt卻不行,那就是txt檔案的內容在輸入StandardInput的時候發生了問題。
想要知道發生什麼問題,過程中還試了StandardError,要看看是不是發生什麼錯誤輸出了錯誤訊息到這裡面,不過從內容中去找尋線索未果,不過在官方文件上看到有可能發生死結的情況,不知道為何卻想到試著改用內容較少的txt檔案,在同樣的程式碼運行卻可以正常。
最終在多個不同的假設測試下,發現會卡住的原因是txt檔案太大,內容過多的情況下會卡在Stream.CopyTo這個環節,所以判斷可能是在複製到StandardInput的過程中也會將處理的結果輸出到StandardOutput,但是StandardOutput一直收收收,收到緩衝區滿了沒處理,CopyTo這裡傳給Tika,Tika說「你等等,我在等Output處理」,我們的程式是在後面才呼叫StandardOutput.ReadToEnd,所以在Stream.CopyTo的這裡一直等Tika接收完內容,最終形成了死結…
處理這個問題不困難,除了StandardOutput.ReadToEnd這個方式處理內容之外,它還有個非同步的處理方式,在前面先建立一個StringBuilder來接收內容,最後再改成從StringBuilder取得所有內容,結果就是相同的。最後修改後的程式碼如下:
public string Extract(Stream stream)
{
var Arg = $"-jar {TikaPath} -t --encoding=UTF-16 -";
//run Tika
StringBuilder output = new StringBuilder();
var p = new Process();
p.StartInfo.FileName = JavaPath;
p.StartInfo.Arguments = Arg;
p.StartInfo.CreateNoWindow = true;
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardInput = true;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.StandardOutputEncoding = Encoding.BigEndianUnicode;
p.OutputDataReceived += new DataReceivedEventHandler((s, e) => output.AppendLine(e.Data));
p.Start();
p.BeginOutputReadLine();
var stdin = p.StandardInput.BaseStream;
stream.CopyTo(stdin);
stdin.Close();
p.WaitForExit();
return output.ToString();
}
修改後的內容改用OutputDataReceived事件處理每次收到的Output內容放入StringBuilder,只是這邊有個地方要特別注意,那就是StandardOutput的Encoding要特別設定,不然結果會和使用StandardOutput.ReadToEnd的內容不同(編碼不同)。
參考資訊:
- https://docs.microsoft.com/zh-tw/dotnet/api/system.diagnostics.process.standarderror
- https://docs.microsoft.com/zh-tw/dotnet/api/system.diagnostics.process.standardinput
- https://stackoverflow.com/questions/26384772/deadlock-while-writing-to-process-standardinput
- https://stackoverflow.com/questions/36918007/beginoutputreadline-and-encoding
- https://docs.microsoft.com/en-us/sysinternals/downloads/procmon
- https://blog.darkthread.net/blog/977/