UNSW CSE의 autotest와 give는 학교 서버에서만 실행할 수 있다. 그래서 로컬 디렉터리를 서버에 동기화하고 명령을 실행한 뒤 출력을 가져오는 작은 도구를 Rust로 만들었다.
UNSW CSE 과제에서는 거의 항상 두 가지 명령을 쓴다. autotest는 테스트를 실행하고, give는 과제를 제출한다. 그런데 이 명령들은 학교 서버에만 있어서, 어디서 코드를 작성하든 마지막에는 CSE 서버에 로그인해서 실행해야 한다.
이번 학기에 COMP6991에서 Rust를 배우고 있다. 연습 삼아 작은 도구를 하나 만들어 보고 싶었는데, 마침 이 문제가 적당해 보여서 CSERun을 만들었다.
CSERun: 로컬 터미널에서 CSE 서버의 명령 실행.
로컬 개발의 번거로움
서버에서 바로 코드를 작성한다면 별문제는 없다. vim을 써도 되고 VS Code의 원격 개발 기능을 써도 된다. 하지만 나를 포함해 로컬에서 작업하는 걸 더 좋아하는 사람도 많다. 에디터와 각종 설정은 이미 준비되어 있고, 서버는 마지막에 테스트할 때만 필요하다.
그러면 코드를 수정할 때마다 번거롭게 다음 과정을 반복해야 한다.
로컬에서 코드를 수정한다.
scp나 rsync로 파일을 서버에 보내거나, git push를 한 뒤 서버에서 pull한다.
ssh로 로그인하고 해당 디렉터리로 이동한다.
autotest를 실행하고 결과를 확인한다.
로컬로 돌아와 계속 수정한다.
원했던 사용 방식
원하는 건 단순했다. 로컬 과제 디렉터리에서 명령 하나로 끝내고 싶었다.
Shell
cd ~/COMP6991/lab01/exercise01
cserun "6991 autotest"
도구가 알아서 서버에 연결하고 로그인한 뒤, 현재 디렉터리를 업로드하고 그곳에서 명령을 실행하면 된다. 마지막으로 출력은 로컬 터미널에 그대로 보여 주면 된다.
실행할 때마다 새 디렉터리 사용하기
이전 파일이 영향을 주지 않도록 실행할 때마다 서버에 새 디렉터리를 만든다. 현재 시각을 이름으로 삼아 ~/.cserun/temp/ 아래에 생성하고, 업로드한 파일은 그 안의 container에 넣는다. 실행한 명령도 command.txt에 기록해서, 나중에 무엇을 실행했는지 확인할 수 있게 했다.
Rust
// 用当前时间做目录名,比如 .cserun/temp/2024-02-14-01-10-40-224/
let temp_dir_name = chrono::Local::now()
.format("%Y-%m-%d-%H-%M-%S-%3f")
.to_string();
let remote_dir = format!(".cserun/temp/{}", temp_dir_name);
명령은 container 안에서 실행하므로, 명령 입장에서는 깨끗한 과제 디렉터리에서 실행되는 셈이다.
연결과 업로드에는 libssh2를 감싼 ssh2 라이브러리를 쓴다. 로그인, SFTP, 명령 실행을 모두 처리할 수 있다.
어떤 파일을 보낼지 정할 때는 ripgrep에서도 사용하는 ignore 라이브러리로 로컬 디렉터리를 순회한다. .gitignore와 .ignore를 자동으로 읽는 것이 장점이다. target이나 node_modules 같은 디렉터리가 이미 .gitignore에 있으면 업로드하지 않으므로, 제외 규칙을 따로 한 번 더 작성할 필요가 없다.
Rust
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)?;
}
}
}
물론 6991 classrun -sturec처럼 로컬 파일이 필요 없는 명령도 있다. 그래서 --no-sync 옵션을 추가했다. 이 옵션을 쓰면 업로드를 건너뛰고 빈 디렉터리만 만든다.
로그인
로그인 정보는 ~/.config/cserun/config.toml 설정 파일에 넣도록 했다. 처음 실행할 때 파일이 없으면 프로그램이 템플릿을 자동으로 만들고, 내용을 채우라는 안내를 표시한다.
인증 방식은 비밀번호, 키 파일, ssh-agent 세 가지를 지원한다. Rust에서는 이를 하나의 열거형으로 나타내고, 각 방식에 필요한 데이터를 해당 변형에 바로 넣을 수 있다.
명령줄 인자는 clap의 derive로 구현했다. 구조체에 속성을 몇 개 붙이면 도움말과 인자 파싱이 모두 준비된다.
Rust
#[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,
}
인자 간 관계도 속성에서 바로 선언할 수 있다. 예를 들어 --no-sync와 --env는 명령과 함께 사용해야 하고, --config는 단독으로만 사용할 수 있다. 이런 조건을 직접 검사할 필요가 없다.
UNSW CSE 과제에서는 거의 항상 두 가지 명령을 쓴다. autotest는 테스트를 실행하고, give는 과제를 제출한다. 그런데 이 명령들은 학교 서버에만 있어서, 어디서 코드를 작성하든 마지막에는 CSE 서버에 로그인해서 실행해야 한다.
이번 학기에 COMP6991에서 Rust를 배우고 있다. 연습 삼아 작은 도구를 하나 만들어 보고 싶었는데, 마침 이 문제가 적당해 보여서 CSERun을 만들었다.
로컬 개발의 번거로움
서버에서 바로 코드를 작성한다면 별문제는 없다. vim을 써도 되고 VS Code의 원격 개발 기능을 써도 된다. 하지만 나를 포함해 로컬에서 작업하는 걸 더 좋아하는 사람도 많다. 에디터와 각종 설정은 이미 준비되어 있고, 서버는 마지막에 테스트할 때만 필요하다.
그러면 코드를 수정할 때마다 번거롭게 다음 과정을 반복해야 한다.
원했던 사용 방식
원하는 건 단순했다. 로컬 과제 디렉터리에서 명령 하나로 끝내고 싶었다.
도구가 알아서 서버에 연결하고 로그인한 뒤, 현재 디렉터리를 업로드하고 그곳에서 명령을 실행하면 된다. 마지막으로 출력은 로컬 터미널에 그대로 보여 주면 된다.
실행할 때마다 새 디렉터리 사용하기
이전 파일이 영향을 주지 않도록 실행할 때마다 서버에 새 디렉터리를 만든다. 현재 시각을 이름으로 삼아 ~/.cserun/temp/ 아래에 생성하고, 업로드한 파일은 그 안의 container에 넣는다. 실행한 명령도 command.txt에 기록해서, 나중에 무엇을 실행했는지 확인할 수 있게 했다.
명령은 container 안에서 실행하므로, 명령 입장에서는 깨끗한 과제 디렉터리에서 실행되는 셈이다.
파일 동기화
연결과 업로드에는 libssh2를 감싼 ssh2 라이브러리를 쓴다. 로그인, SFTP, 명령 실행을 모두 처리할 수 있다.
어떤 파일을 보낼지 정할 때는 ripgrep에서도 사용하는 ignore 라이브러리로 로컬 디렉터리를 순회한다. .gitignore와 .ignore를 자동으로 읽는 것이 장점이다. target이나 node_modules 같은 디렉터리가 이미 .gitignore에 있으면 업로드하지 않으므로, 제외 규칙을 따로 한 번 더 작성할 필요가 없다.
물론 6991 classrun -sturec처럼 로컬 파일이 필요 없는 명령도 있다. 그래서 --no-sync 옵션을 추가했다. 이 옵션을 쓰면 업로드를 건너뛰고 빈 디렉터리만 만든다.
로그인
로그인 정보는 ~/.config/cserun/config.toml 설정 파일에 넣도록 했다. 처음 실행할 때 파일이 없으면 프로그램이 템플릿을 자동으로 만들고, 내용을 채우라는 안내를 표시한다.
인증 방식은 비밀번호, 키 파일, ssh-agent 세 가지를 지원한다. Rust에서는 이를 하나의 열거형으로 나타내고, 각 방식에 필요한 데이터를 해당 변형에 바로 넣을 수 있다.
그런 다음 match로 각각 처리한다. 인증 방식을 새로 추가하거나 빠뜨리면 컴파일러가 알려 준다.
비밀번호 인증을 선택했지만 설정 파일에 비밀번호가 없으면, 실행 중 rpassword로 입력을 요청한다. 입력한 내용은 화면에 표시되지 않는다.
출력
명령이 실행되는 동안에는 로컬에서 실행할 때처럼 출력이 바로바로 보이면 좋겠다. 전부 끝날 때까지 기다렸다가 한꺼번에 출력하고 싶지는 않았다. 그래서 연결을 논블로킹으로 설정하고, 루프에서 표준 출력과 표준 오류를 계속 읽어 데이터가 들어오는 즉시 출력하도록 했다.
논블로킹 상태에서 데이터가 없으면 WouldBlock 오류가 반환된다. 이는 아직 읽을 데이터가 없다는 뜻이므로, 별도로 매칭해서 건너뛰면 된다.
명령에 환경 변수를 전달하는 --env 옵션도 추가했다. 여기서 작은 문제가 있었다. CSE 서버에서는 libssh2의 setenv가 항상 동작하는 것은 아니었다. 그래서 실패하면 명령 앞에 export를 붙이는 방식으로 처리했다.
명령줄 인자
명령줄 인자는 clap의 derive로 구현했다. 구조체에 속성을 몇 개 붙이면 도움말과 인자 파싱이 모두 준비된다.
인자 간 관계도 속성에서 바로 선언할 수 있다. 예를 들어 --no-sync와 --env는 명령과 함께 사용해야 하고, --config는 단독으로만 사용할 수 있다. 이런 조건을 직접 검사할 필요가 없다.
사용해 보기
Rust가 설치되어 있다면 명령 하나로 설치할 수 있다.
설치한 뒤에는 과제 디렉터리에서 실행하면 된다. 다음은 파일 동기화를 건너뛰고 환경 변수를 전달하는 예다.
평소에는 cserun "6991 autotest"처럼 명령 하나면 된다. 로컬에서 코드를 수정한 뒤 바로 실행하면 테스트 결과가 로컬 터미널에 나타난다.