0%

[DotnetCore]Batch檔案執行及追縱

前情提要

筆者因為工作環境需要跟多個系統做串接,因最近參與的大系統,必須在某一塊作業使用外部廠商系統,需要透過各種介接方式做資料上的串接,例如﹔需要與廠商平台上面撰寫外掛程式,呼叫我們寫好的API,做為資料同步,這不在這篇的講的內容就不往下深入討論,另一種可能是我們在某些情境下,某個時段,需要驅動廠商系統進行其作業,廠商系統也有開放batch file,讓我們方便去執行。

上述中的第二種執行batch file變得格外繁瑣,為何這樣說呢,讓筆者娓娓道來,為各系統間不要互相影響,一定將各系統隔開,,試想若把各種不同系統都放在同一台,若該台伺服器遇到甚麼問題,其不就是自找死路,完全運作不下去。基於各種理由會將外部廠商系統獨立於一台伺服器中,這樣問題就來了,第一是batch file是一個落地的檔案,要由另一台伺服器(筆者部門的系統)去觸發執行該batch file,且也要能夠追蹤其執行狀況,因為有可能該bat作業會產生檔案,筆者部門系統需要該檔案做後續作業。

內容

介紹完情境後,開始來思考一件事情,我們需要完成哪些事情,如下:

  • 通知執行
  • 執行Batch file
  • 執行結果監聽

通知執行

通知執行部分,由諸多選擇,利用SignalR類的 web socket通訊、利用Message Queue、利用資料庫pooling方式,基本上前兩種為留下軌跡紀錄,必須搭配資料庫做紀錄查詢,筆者這邊索性選擇最後一種資料庫Pooling的方式,當然也可以透過SqlDependency的方式監聽資料表的變化,因該方式牽扯到資料庫權限問題,筆者公司環境不允許AP端擁有過高的資料庫權限,僅能有讀寫權限,只能放棄作罷。筆者會會再寫一篇單獨介紹SqlDependency的文章,到時再詳細討論。

基本上是設計一張資料表存放欲執行的batch file路徑及檔名,以及執行完成後產生的檔案路徑及名稱,筆者工作環境需要由產生檔案的資料做一些邏輯處理,會再加一些執行Store Procedure的設定欄位。對應物件設計如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public partial class BatchLog
{
public string BatchId { get; set; }
public string BatFilePath { get; set; }
public string BatFileName { get; set; }
// batch file執行完畢後產生檔案路徑及名稱
public string ResultFilePath { get; set; }
public string ResultFileName { get; set; }
// 因為要解析產生檔案,資料部分可能會有表頭,其設定為資料列從哪一列開始
public int? DataStartRow { get; set; }
// 以下為筆者工作環境中用到的解析邏輯相關設定,這就因人而異
public string CapPublishName { get; set; }
public string CapPublishParaFname { get; set; }
public string CapPublishParaDataDate { get; set; }
public string Result { get; set; }
public string ErrorMessage { get; set; }
public string AuditUser { get; set; }
public DateTime? AuditTime { get; set; }
}

執行Batch File

基本上外部廠商系統,已把執行邏輯包成一個batch file,筆者負責部份使用command line模式下執行該batch file即可。需要用到ProcessStartInfoProcess這兩個類別,參考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public void Execute()
{
var processStartInfo = new ProcessStartInfo("cmd.exe", $"/c {_batchLog.BatFileName}");
processStartInfo.CreateNoWindow = true;
processStartInfo.UseShellExecute = false;
processStartInfo.RedirectStandardError = true;
processStartInfo.RedirectStandardOutput = true;
processStartInfo.WorkingDirectory = _nxbatLog.BatFilePath;

var _process = Process.Start(processStartInfo);
_process.OutputDataReceived += OutputDataReceived;
_process.ErrorDataReceived += ErrorDataReceived;
_process.EnableRaisingEvents = true;
_process.Exited += Exited;
_process.BeginErrorReadLine();
_process.BeginOutputReadLine();
_process.WaitForExit();
_process.Close();
}
private void ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
// logic
}

private void OutputDataReceived(object sender, DataReceivedEventArgs e)
{
// logic
}

private void Exited(object sender, EventArgs e)
{
// logic
}

執行結果監聽

上節式碼部份,需要說明一下,筆者測驗的結果,需要配合設定才會有相對應的event事件觸發,須注意一下有些是針對ProcessStartInfo的設定,有些是對Process的設定,需釐清楚:

Error通知事件

Error通知事件要特別注意,經由筆者測試發現,即便batch file執行完成後,exitcode0,仍觸發ErrorDataReceived事件,最保險的方式是裡頭再判斷string.IsNullOrEmpty(e.Data) == false才做接下來要做的事情。

1
2
3
4
5
6
7
8
9
// 須將該設定設為true
processStartInfo.RedirectStandardError = true;
// 以下掛載event才會作用
_process.ErrorDataReceived += ErrorDataReceived;
// event觸發後處理邏輯
private void ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
// logic
}

Output通知事件

1
2
3
4
5
6
7
8
9
// 須將該設定設為true
processStartInfo.RedirectStandardOutput = true;
// 以下掛載event才會作用
_process.OutputDataReceived += OutputDataReceived;
// event觸發後處理邏輯
private void OutputDataReceived(object sender, DataReceivedEventArgs e)
{
// logic
}

結束通知事件

1
2
3
4
5
6
7
8
9
// 須將該設定設為true
_process.EnableRaisingEvents = true;
// 以下掛載event才會作用
_process.Exited += Exited;
// event觸發後處理邏輯
private void Exited(object sender, EventArgs e)
{
// logic
}

筆者不免俗的利用event, action對最終端使用開放擴充性,附上完整程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class MyBatchJob : IDisposable
{
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
private readonly BatchLog _batchLog;
public event Action<BatchLog , DataReceivedEventArgs> MyOutputDataReceived;
public event Action<BatchLog , DataReceivedEventArgs> MyErrorDataReceived;
public event Action<BatchLog , int, EventArgs> MyExited;
private Process _process;

public MyBatchJob(BatchLog batchLog)
{
_batchLog= batchLog;
}
public void Execute()
{
var processStartInfo = new ProcessStartInfo("cmd.exe", $"/c {_batchLog.BatFileName}");
processStartInfo.CreateNoWindow = true;
processStartInfo.UseShellExecute = false;
processStartInfo.RedirectStandardError = true;
processStartInfo.RedirectStandardOutput = true;
processStartInfo.WorkingDirectory = _batchLog.BatFilePath;

_process = Process.Start(processStartInfo);
_process.OutputDataReceived += OutputDataReceived;
_process.ErrorDataReceived += ErrorDataReceived;
_process.EnableRaisingEvents = true;
_process.Exited += Exited;
_process.BeginErrorReadLine();
_process.BeginOutputReadLine();
_process.WaitForExit();
_process.Close();
}

private void ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
MyErrorDataReceived(_batchLog, e);
}

private void OutputDataReceived(object sender, DataReceivedEventArgs e)
{
MyOutputDataReceived(_batchLog, e);
}

private void Exited(object sender, EventArgs e)
{
MyExited(_batchLog, _process.ExitCode, e);
}

public void Dispose()
{
_logger.Info($"[MyBatchJob Dispose]!!");
_process.Dispose();
// Suppress finalization.
GC.SuppressFinalize(this);
}
}

Client端使用

最終使用端使用起來也很簡便,如下:

1
2
3
4
5
6
// item為BatchLog類別
var myBatchJob = new MyBatchJob(item);
myBatchJob .MyOutputDataReceived += MyOutputDataReceived;
myBatchJob .MyErrorDataReceived += MyErrorDataReceived;
myBatchJob .MyExited += MyExited;
myBatchJob .Execute();

結論

筆者因工作環境需要執行batch file,藉機熟悉一下平常比較少使用到的ProcessStartInfoProcess,也因資料庫Pooling及Log,徹底研究了一下對應事件觸發及其設定方式,可以說是以後有類似的需求時,不用再爾外花時間研究其運作方式,基本上筆者程式碼片段中,可以將BatchLog改成泛型T基本上可以變成通用型,也不怕每份工作的不同資料結構設計。

參考