let walker = WalkBuilder::new(local_path)
.ignore(true)
.git_ignore(true)
.build();
for entry in walker.flatten() {
let path = entry.path();
if let Ok(strip_path) = path.strip_prefix(local_path) {
let remote_path = remote_base_path.join(strip_path);
if path.is_dir() {
sftp.mkdir(&remote_path, 0o755)?;
} else {
upload_file(sftp, path, &remote_path)?;
}
}
}
#[derive(Parser, Debug)]
#[clap(version, about, long_about = None)]
struct Args {
/// Do not sync files before running the command
#[clap(long, requires = "command")]
no_sync: bool,
/// Set environment variables, in the format of KEY:VALUE
#[clap(long, value_name = "KEY:VALUE", value_parser = parse_env, requires = "command")]
env: Vec<String>,
/// The command to run on the cse server
command: Option<String>,
/// Show the path of config file
#[clap(long, conflicts_with_all = &["no_sync", "env", "command"])]
config: bool,
}
UNSW CSE の課題では、ほぼ必ず二つのコマンドを使います。テストを実行する autotest と、課題を提出する give です。ただし、どちらも大学のサーバーにしかありません。どこでコードを書いても、最後は CSE のサーバーにログインして実行する必要があります。
今学期は COMP6991 で Rust を学んでいます。練習用に何か小さなものを作りたいと思っていたので、この用途はちょうどよさそうでした。そうして作ったのが CSERun です。
手元で開発すると面倒なこと
最初からサーバー上でコードを書くなら、特に問題はありません。vim でも、VS Code のリモート機能でも作業できます。ただ、私のように手元で書くほうが好きな人もいます。エディターや設定はすでに整っていて、サーバーが必要なのは最後にテストを実行するときだけです。
そうすると、コードを直すたびに次の作業が必要になります。
こう使いたい
やりたいことは単純です。手元の課題ディレクトリで、一つのコマンドだけで済ませたい。
サーバーへの接続とログイン、現在のディレクトリの転送、リモートでのコマンド実行までを自動で行い、その出力をそのまま手元のターミナルに表示する、という形です。
実行するたびに新しいディレクトリを使う
古いファイルが邪魔をしないように、実行するたびにサーバー上に新しいディレクトリを作ります。現在時刻を名前にして ~/.cserun/temp/ の下に作り、転送したファイルはその中の container に置きます。実行したコマンドも command.txt に記録しておけば、あとから何を実行したのか確認できます。
コマンドは container の中で実行します。コマンドから見れば、余計なファイルのない課題ディレクトリで実行しているのと同じです。
ファイルの同期
接続と転送には ssh2 を使いました。libssh2 のラッパーで、認証、SFTP、コマンドの実行まで扱えます。
転送するファイルを選ぶために、手元のディレクトリの走査には ignore を使いました。ripgrep でも使われているライブラリです。.gitignore と .ignore を自動で読み込むので、target や node_modules などがすでに .gitignore に書かれていれば転送されません。除外ルールを別に書き直す必要がないのが便利です。
ただ、6991 classrun -sturec のように、ローカルのファイルを使わないコマンドもあります。そのために --no-sync オプションを追加しました。これを付けると転送を省き、空のディレクトリだけを作ります。
ログイン
ログイン情報は ~/.config/cserun/config.toml に置く設定ファイルにまとめました。初回実行時にファイルがなければ、テンプレートを自動生成して、内容を記入するよう案内します。
認証方法はパスワード、鍵ファイル、ssh-agent の三つに対応しています。Rust なら、この三つを一つの enum で表せます。それぞれの認証に必要なデータも、対応するバリアントに持たせられます。
あとは match でそれぞれ処理します。認証方法を追加したときなどに処理が抜けていれば、コンパイラーが教えてくれます。
パスワード認証を選んでも設定ファイルにパスワードがなければ、実行時に rpassword で入力を求めます。入力内容は画面に表示されません。
出力
コマンドの出力は、全部終わってからまとめて表示するのではなく、手元で実行するときのように、動きに合わせて表示したいと思いました。そこで接続をノンブロッキングにし、ループ内で標準出力と標準エラーを読み続け、読めた分をすぐに表示します。
ノンブロッキングでは、まだデータがないと WouldBlock エラーが返ります。この場合は一時的に読めるデータがないだけなので、個別に判定し、次のループで再度読み取ります。
さらに、コマンドに環境変数を渡す --env オプションも付けました。ここでは少し問題がありました。CSE のサーバーでは libssh2 の setenv が使えるとは限りません。そこで、失敗したときはコマンドの前に export を付ける方法に切り替えました。
コマンドライン引数
引数の処理には clap の derive を使いました。構造体にいくつか属性を付けるだけで、ヘルプ表示と引数の解析を用意できます。
引数同士の関係も属性で宣言できます。たとえば、--no-sync と --env はコマンドと一緒に使う必要があり、--config は単独でしか使えません。こうした判定を自分で書かずに済みます。
使ってみる
インストールは一つのコマンドで済みます。先に Rust をインストールしておきます。
インストール後は、課題のディレクトリで実行できます。次はファイルの同期を省き、環境変数を渡す例です。
普段は cserun "6991 autotest" のように使うだけです。手元でコードを修正したら、そのまま実行すれば、テスト結果が手元のターミナルに表示されます。