Boost Process V2 的輸入輸出

| | 0 Comments| 10:13|
Categories:

前面一篇已經把 Boost Process v2 的大部分東西都大概紀錄了一下。而在外部程式的輸入、輸出的部分,Process 也有透過 initializer 的形式來提供多種設定、使用的方法,這邊就獨立一篇出來講吧。

首先,process v2 是提供 process_stdio 這個結構,讓使用者可以針對輸入(in)、輸出(out)、錯誤(err)分別進行設定、決定要處理的方法。

而可以使用的形式,主要包括:

  • asio pipe
  • C 的檔案指標 FILE*
  • 指定檔案路徑
  • 忽略輸出的 nullptr
  • 個平台原生的 handler

而基本的使用,會是下面的形式:

boost::process::process proc(ctx,
  "c:\\windows\\system32\\ping.exe",
  { "www.google.com", "-n", "4" },
  boost::process::process_stdio{ {}, {}, {} });

這邊在建立 process_stdio 的物件的時候,給了三個空的參數,分別代表輸入、輸出、錯誤;而像這樣給 {} 就代表是給預設值,所以這邊其實有給和沒給是一樣的。

而如果想要修改輸入、輸出的方法的話,基本上只要修改 process_stdio 建構時的三個參數就可以了。

不過這邊先提一下,Heresy 在測試的時候,是發現 Windows 環境下似乎沒辦法把 outerr 設定給同一個目標?這個不知道是不是實作上的限制?


FILE*

如果是要比較容易理解的操作,應該是使用 C 的檔案指標?這邊也支援 stdinstdoutstderr 這些巨集(參考),所以如果只是要做重新導向的話,算是滿方便的。

像是下面這段程式:

boost::process::process proc(ctx,
  "/usr/bin/ping",
  { "www.google.com", "-c", "4" },
  boost::process::process_stdio{ {}, stdout, stdout });

這邊透過 process_stdio 來讓正常的輸出和錯誤的輸出都丟給 stdout

不過這樣的程式雖然在 Linux 雖然可以正確執行,但是就像前面提到的、在 Windows 下似乎是會在執行時出錯的


而如果是想要把結果輸出到檔案,也可以寫成:

FILE* fp = fopen("/tmp/test.txt", "w+");

boost::asio::io_context ctx;
boost::process::process proc(ctx,
  "/usr/bin/ping",
  { "www.google.com", "-c", "4" },
  boost::process::process_stdio{ {}, fp, fp });

proc.wait();

fclose(fp);

這樣的程式就會把 ping 的結果和錯誤都儲存在 /tmp/test.txt 這個檔案了。

而這邊也可以用讀取模式開啟一個檔案、把它當成標準輸入丟給外部程式,寫法基本上是類似的。


檔案路徑

如果只是要輸出到檔案、或是從檔案讀取的話,其實 process_stdio 也可以直接透過檔案路徑來設定、不需要手動控制 FILE*;不過,這邊的路徑應該只能用 boost::filesystem::path 就是了。

比如說上面的程式碼可以改寫成:

boost::process::process proc(ctx,
  "/usr/bin/ping",
  { "www.google.com", "-c", "4" },
  boost::process::process_stdio{ {}, boost::filesystem::path("/tmp/test.txt"), {} });

這樣也是會把結果輸出到檔案裡的。(不過這邊就沒管錯誤了)


忽略輸出

而如果不想管輸出的話,其實也可以直接設定成 nullptr,這樣就可以直接無視了。

像寫成下面的樣子的話,基本上就不會看到 ping 的結果了。

boost::process::process proc(ctx,
  "/usr/bin/ping",
  { "www.google.com", "-c", "4" },
  boost::process::process_stdio{ {}, nullptr, {} });

而根據官方文件的說法,這樣寫在 POSIX 系統上會是丟給 /dev/null、在 Windows 則是丟給 NUL


asio pipe

如果要進行比較複雜的操作的話,這邊可能就要使用 Boost asio 的 pipe 了。下面是根據官方的範例,把之前 ping 的範例、修改成用 readable_pipe 讀取的程式;這邊為了讓 Windows 和 Linux 都能執行,其實還滿麻煩的…

#ifdef _WIN32
#include <WinSock2.h>
#pragma comment(lib, "ntdll.lib")
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "shell32.lib")
#endif
 
#include <iostream>
#include <boost/asio/read.hpp>
#include <boost/process.hpp>
 
#ifdef _WIN32
const std::string sPara = "-n";
#else
const std::string sPara = "-c";
#endif
 
int main()
{
  boost::asio::io_context ctx;
 
  boost::asio::readable_pipe rp{ ctx };
 
  boost::process::process proc(ctx,
    boost::process::environment::find_executable("ping"),
    { "www.google.com", sPara, "4" },
    boost::process::process_stdio{ {}, rp, {} });
 
  boost::system::error_code ec;
  std::string output;
  boost::asio::read(rp, boost::asio::dynamic_buffer(output), ec);
  if (ec == boost::asio::error::eof ||        // Linux
    ec == boost::asio::error::broken_pipe)  // Windows
  {
    std::cout << output << std::endl;
  }
  else
  {
    std::cerr << "Error: " << ec.message() << std::endl;
  }
  proc.wait();
}

這邊會先建立一個 readable_pipe 的物件 rp,之後則是讓 process 把本來的標準輸出都丟到 rp 裡面。

在執行後,則是透過 boost::asio_read() 把資料全部讀到 output 這個字串裡面,確認沒問題後再整個輸出。

不過在 Windows 上正常結束的時候,ec 似乎不會是 boost::asio::error::eof、而都是 boost::asio::error::broken_pipe?這點不確定是什麼的問題。


不過這樣的讀取方法會一次讀完、所以 ping 的結果會一次讀取到字串後再統一輸出,和本來一次一次輸出會有相當地差別。

這樣的效果在不少使用情境下可能不見得適合,如果不想要這樣,就得用別的讀取函示來逐步讀取了。個人覺得比較直覺的方法,應該會是逐行讀取?但是很可惜的,是 Boost asio 相關的讀取函式似乎沒有類似 getline() 的方法,所以要把輸出的結果一行一行處理其實相對還滿麻煩的。


而如果想要給輸入用的話,則是可以使用 asio 的 writable_pipe 來操作。下面是一個簡單的例子:

boost::asio::io_context ctx;

boost::asio::writable_pipe wp{ ctx };
boost::asio::readable_pipe rp{ ctx };

boost::process::process proc(ctx,
  "./echo", {},
  boost::process::process_stdio{ wp, rp, {} });

std::string line;
boost::asio::write(wp, boost::asio::buffer("test1\n"));
boost::asio::read_until(rp, boost::asio::dynamic_buffer(line), '\n');
std::cout << "echo: [" << line << "]\n";
boost::asio::write(wp, boost::asio::buffer("exit\n"));

這邊是去呼叫一個自己寫的 echo 程式、輸入什麼就會回傳什麼。

而之後則是會送一個「test\n」的字串,然後讀取他的輸出值後再透過 std::out 輸出;最後則是在透過送出一個「exit\n」來結束 echo 這個程式。

不過實際上,這邊的程式只是示意、還是有些小問題的,實務上要做到實用還有不少細節要修改就是了。


popen

如果要用 pipe 來控制輸入和輸出的話,除了透過 process_stdio 來設定外,process 還有另外提供一個稍微簡單一點的方法,那就是 popen

popen 是一個 process 的擴展型別、使用方法基本上和 process 相同,不過預設就會把標準輸入和標準輸出改掉(沒有修改錯誤的部分),而本身就可以透過 asio 相關的函式還做讀寫,所以算是可以簡化 process 的設定。

以前面 echo 的例子來說,可以改寫成:

boost::asio::io_context ctx;

boost::process::popen proc(ctx, "./echo", {});

std::string line;
boost::asio::write(proc, boost::asio::buffer("test1\n"));
boost::asio::read_until(proc, boost::asio::dynamic_buffer(line), '\n');
std::cout << "echo: [" << line << "]\n";
boost::asio::write(proc, boost::asio::buffer("exit\n"));

這邊基本上就是把 proc 的型別從 process 改成 popen 而已,之後就 asio 的 read、write 等函式就可以直接對 proc  來進行操作了。

這樣的寫法其實和直接使用 asio 的 pipe 沒有什麼差別,除了省去初始化的設定外,個人覺得最大的好處就是不用去處理 pipe 的物件。


Native handle

除了上面的東西外,Process 也可以針對系統原生的 handler 來做處理。在 POSIX 的系統上會是 int 這種 file descriptor,在 Windows 上則是檔案的 HANDLE

此外,只要是可以透過 native_handle() 這個函式取得 stdio stream 可用的型別的物件、也都可以搭配 process_stdio 來使用。

不過這邊基本上就先無視這部份了。


Boost Process 就先這樣了。

其實本來還有想要針對 pipe 的操作繼續寫下去,但是後來想想那部份應該算是 asio 的東西,所以就不寫在這一篇了;之後如果有機會,再來研究看看怎麼把 asio 的 pipe 轉換成 io stream 來做存取吧。

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *