Skip to content

Commit

Permalink
Allow aborting various commands with Ctrl+C
Browse files Browse the repository at this point in the history
  • Loading branch information
Bubbler-4 committed Jun 27, 2024
1 parent 8d92d2f commit 3000006
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 21 deletions.
31 changes: 30 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "gaboja"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
license = "MIT"
description = "Gaboja: CLI helper for solving BOJ problems"
Expand All @@ -15,6 +15,7 @@ categories = ["command-line-utilities"]
[dependencies]
anyhow = "1.0.82"
console = "0.15.8"
ctrlc = "3.4.4"
dialoguer = { version = "0.11.0", features = ["history"], default-features = false }
indicatif = "0.17.8"
once_cell = "1.19.0"
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
* 윈도우 빌드의 경우 바이러스로 인식이 된다거나 잘 동작하지 않거나 할 수 있습니다. 그런 경우에는 issue를 열어 주세요.
* 설치하고 실행하기 전에 터미널을 재시작해야 할 수 있습니다.

### 업데이트 방법

* `cargo install gaboja`로 설치한 경우 같은 커맨드로 업데이트가 가능합니다.
* 바이너리를 다운받아서 설치한 경우 바이너리를 새로 받아 덮어쓰면 됩니다.

## 사용 방법

터미널을 켜서, 문제를 푸는 코드를 작성할 폴더 위치에서 gaboja를 실행합니다. 조금 기다리면 `BOJ >`라는 프롬프트가 나타납니다. 여기서 `help`를 입력하여 사용 가능한 커맨드를 확인할 수 있습니다.
Expand Down Expand Up @@ -76,6 +81,9 @@ test [c=CMD]
# 소스를 문제에 제출하고 결과를 확인합니다.
submit [l=LANG] [f=FILE]
# 다른 터미널을 켤 필요 없이 임의의 셸 커맨드를 실행할 수 있습니다.
$ <shellcmd>
# firefox와 geckodriver를 끄고 gaboja를 종료합니다.
exit
```
Expand Down Expand Up @@ -112,3 +120,11 @@ input = 'input.txt'
lang = 'Python 3'
file = 'src.py'
```

## Ctrl+C 동작

커맨드 입력 대기 상태에서 Ctrl+C를 입력하면 exit을 입력한 것처럼 gaboja를 종료합니다.

build, run, test 도중에 Ctrl+C를 입력하면 커맨드에 의해 실행된 프로그램의 실행이 중단됩니다. gaboja는 중단되지 않습니다.

submit에서 채점 진행 중에 Ctrl+C를 입력하면 채점 상황 업데이트를 중단하고 커맨드 입력 대기 상태로 돌아옵니다.
11 changes: 10 additions & 1 deletion src/command/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,14 +372,18 @@ impl GlobalState {

let spinner = Spinner::new("Submitting code...");
self.browser.submit_solution(prob, &source, lang)?;
spinner.finish("Code submitted.");
spinner.finish("Code submitted. Press Ctrl+C to stop watching submission status.");

let submit_progress = SubmitProgress::new();
loop {
let (status_text, status_class) = self.browser.get_submission_status()?;
if submit_progress.update(&status_text, &status_class) {
break;
}
if self.ctrlc_channel.try_recv().is_ok() {
self.ctrlc_channel.try_iter().count();
break;
}
}
Ok(())
}
Expand Down Expand Up @@ -417,4 +421,9 @@ help
Display this help.
exit
Exit the program.
$ <shell cmd>
Run an arbitrary shell command.
^C
During build/run/test, kill the running program.
During submit, stop watching the submission status.
";
7 changes: 7 additions & 0 deletions src/global_state.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::data::{BojConfig, Credentials, Preset, Problem, ProblemId};
use crate::infra::browser::Browser;
use std::collections::HashMap;
use std::sync::mpsc::{channel, Receiver};

pub(crate) struct GlobalState {
pub(crate) credentials: Credentials,
Expand All @@ -14,10 +15,15 @@ pub(crate) struct GlobalState {
pub(crate) browser: Browser,
pub(crate) problem_cache: HashMap<ProblemId, Problem>,
pub(crate) presets: HashMap<String, Preset>,
pub(crate) ctrlc_channel: Receiver<()>,
}

impl GlobalState {
pub(crate) fn new() -> anyhow::Result<Self> {
let (sender, receiver) = channel();
ctrlc::set_handler(move || {
sender.send(()).unwrap();
})?;
let mut state = Self {
credentials: Credentials {
bojautologin: String::new(),
Expand All @@ -33,6 +39,7 @@ impl GlobalState {
browser: Browser::new()?,
problem_cache: HashMap::new(),
presets: HashMap::new(),
ctrlc_channel: receiver,
};
// println!("state initialized");
match BojConfig::from_config() {
Expand Down
15 changes: 5 additions & 10 deletions src/infra/browser.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use crate::data::{ExampleIO, Problem, ProblemId, ProblemKind};
use crate::infra::console::Spinner;
use crate::infra::subprocess::{spawn_cmd_background, run_silent};
use std::future::Future;
use std::process::{Child, Command, Stdio};
use std::process::Stdio;
use thirtyfour::common::cookie::SameSite;
use thirtyfour::prelude::*;
use tokio::runtime;

/// Takes care of interaction with BOJ pages. Internally uses headless Firefox and geckodriver.
pub(crate) struct Browser {
geckodriver: Child,
webdriver: WebDriver,
}

Expand All @@ -28,7 +28,7 @@ impl Browser {
pub(crate) fn new() -> anyhow::Result<Self> {
with_async_runtime(async {
let spinner = Spinner::new("Starting geckodriver...");
let geckodriver = Command::new("geckodriver")
spawn_cmd_background("geckodriver")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
Expand Down Expand Up @@ -57,7 +57,6 @@ impl Browser {

spinner.finish("Browser initialization complete");
Ok(Self {
geckodriver,
webdriver,
})
})
Expand Down Expand Up @@ -220,12 +219,8 @@ impl Browser {
/// Gracefully terminate the browser. Should be called even on error.
pub(crate) fn quit(self) -> anyhow::Result<()> {
with_async_runtime(async {
let Self {
mut geckodriver,
webdriver,
} = self;
webdriver.quit().await?;
geckodriver.kill()?;
self.webdriver.quit().await?;
run_silent("kill $(pidof geckodriver)").ok();
Ok(())
})
}
Expand Down
3 changes: 3 additions & 0 deletions src/infra/console.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ impl SubmitProgress {
impl Drop for SubmitProgress {
fn drop(&mut self) {
if !self.progress_bar.is_finished() {
// message line is duplicated on any keypress (including Ctrl+C), so remove the message line in template as workaround
let style = ProgressStyle::with_template("{bar:40.green}").unwrap();
self.progress_bar.set_style(style);
self.progress_bar.abandon();
}
}
Expand Down
18 changes: 18 additions & 0 deletions src/infra/subprocess.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ fn spawn_cmd(cmd: &str) -> Command {
}
}

/// Background-running processes do not receive Ctrl+C signal from the parent.
/// This is necessary to keep geckodriver alive.
/// On Linux, this is achieved with `<command> &`.
/// On Windows, CMD's START built-in has `/B` option. https://superuser.com/a/591084 This feature is not tested yet.
pub(crate) fn spawn_cmd_background(cmd: &str) -> Command {
if cfg!(target_os = "windows") {
let mut command = Command::new("cmd");
let cmd = format!("START /B \"\" {}", cmd);
command.arg("/C").arg(&cmd);
command
} else {
let mut command = Command::new("sh");
let cmd = format!("{}&", cmd);
command.arg("-c").arg(&cmd);
command
}
}

fn spawn_cmd_tokio(cmd: &str) -> tokio::process::Command {
if cfg!(target_os = "windows") {
let mut command = tokio::process::Command::new("cmd");
Expand Down
33 changes: 25 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,34 @@ fn main() -> anyhow::Result<()> {
let mut state = GlobalState::new()?;

loop {
if let Ok(cmd) = Input::<InputCommand>::with_theme(&ColorfulTheme::default())
let input = Input::<InputCommand>::with_theme(&ColorfulTheme::default())
.with_prompt("BOJ")
.history_with(&mut history)
.interact_text()
{
if cmd.is_exit() {
state.quit()?;
break;
.interact_text();
match input {
Ok(cmd) => {
if cmd.is_exit() {
state.quit()?;
break;
}
if let Err(e) = state.execute(&cmd) {
println!("Error: {}", e);
}
if state.ctrlc_channel.try_recv().is_ok() {
// consume the ctrlc queue
state.ctrlc_channel.try_iter().count();
}
}
if let Err(e) = state.execute(&cmd) {
println!("Error: {}", e);
Err(err) => {
match err {
dialoguer::Error::IO(io_err) => {
if matches!(io_err.kind(), std::io::ErrorKind::Interrupted) {
println!("exit");
state.quit()?;
break;
}
}
}
}
}
}
Expand Down

0 comments on commit 3000006

Please sign in to comment.