diff --git a/Cargo.toml b/Cargo.toml index 0afcb13..2b46f01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,10 @@ whatlang = "0.16.3" isolang = { version = "2.3.0", features = ["list_languages"] } os_info = "3.7.0" async-recursion = "1.0.5" +sysinfo = "0.29.10" +webbrowser = "0.8.12" +duct = "0.13.6" +runas = "1.1.0" # Windows Only [target.'cfg(windows)'.dependencies] diff --git a/conf.example--run-with.toml b/conf.example--run-with.toml new file mode 100644 index 0000000..7564451 --- /dev/null +++ b/conf.example--run-with.toml @@ -0,0 +1,27 @@ +# run_with のお試し用設定ファイルです。 +# お試し方法: virtual-avatar-connect.exe にこのファイルをドラッグ&ドロップまたは、コマンドライン引数で指定して起動してください。 +# ※commandは実際にお試しになる環境で実行可能なコマンドに書き換えるなどしてからお試し下さい。 +# 関連Issue: https://github.com/usagi/virtual-avatar-connect/issues/37 + +run_with = [ + # どうあれメモ帳を起動します。 + "notepad", + + # どうあれURLを開きます。 + "http://127.0.0.1:57000/input", + + # まだ動作していない場合のみ、CoeiroInk を起動します。 + { command = '''C:\Users\the\app\COEIROINK_WIN_GPU_v.2.1.1\COEIROINKv2.exe''', if_not_running = "COEIROINKv2" }, + + # まだ動作していない場合のみ、VirtualMotionCapture を起動します。 + { command = '''C:\Users\the\app\vmc\VirtualMotionCapture.exe''', if_not_running = "VirtualMotionCapture" }, + + # まだ動作していない場合のみ、WebcamMotionCapture を起動します。 + { command = '''C:\Users\the\app\WebcamMotionCapture_Win\bin\webcam_motion_capture\webcam_motion_capture.exe''', if_not_running = "webcam_motion_capture" }, + + # まだ動作していない場合のみ、VMagicMirror を起動します。 + { command = '''"C:\Program Files (x86)\VMagicMirror\VMagicMirror.exe"''', if_not_running = "VMagicMirror" }, + + # まだ動作していない場合のみ、OBS Studio を管理者権限での実行をユーザーに求め、かつ作業ディレクトリーを指定して起動します。 + { command = '''C:\Program Files\obs-studio\bin\64bit\obs64.exe''', if_not_running = "obs64.exe", run_as_admin = true, working_dir = '''C:\Program Files\obs-studio\bin\64bit''' }, +] diff --git a/conf.example-command.toml b/conf.example-command.toml index c25c8fb..95f4aa7 100644 --- a/conf.example-command.toml +++ b/conf.example-command.toml @@ -1,3 +1,9 @@ +# 《Command》 のお試し用設定ファイルです。 +# お試し方法: virtual-avatar-connect.exe にこのファイルをドラッグ&ドロップまたは、コマンドライン引数で指定して起動してください。 +# ※必要に応じて環境にあわせた設定値に変更してお試し下さい。 +# 関連Issue: https://github.com/usagi/virtual-avatar-connect/issues/14 + + # 音声から「かっこよくそれっぽいコマンド」を演出します。(必須ではありませんが心躍る方は参考にして下さい。) [[processors]] feature = "modify" diff --git a/conf.toml b/conf.toml index c411eb5..58630a3 100644 --- a/conf.toml +++ b/conf.toml @@ -29,12 +29,11 @@ workers = 8 # デフォルト: "resources" # web_ui_resources_path = "resources" -# ここで指定したコマンドたちは VAC の起動時に自動的に実行されます。 -# 配信用に同時に使いたいアプリや、開いておきたい URL を設定しておくと便利です。 -# ※ Windows で使う場合は -# 例: "start http://127.0.0.1:57000/input" -# のように設定すると URL を開くのも簡単です。 -# run_with = ["start http://127.0.0.1:57000/input"] +# run_with を設定すると VAC の起動時に他のプログラムを起動したり、URLを開いたりできます。 +# 使い方: conf.example--run_with.conf お試し設定ファイルを参考に設定してみて下さい。 +# 既に起動中なら起動しない、管理者権限で実行(UACあり)などの設定もできます。 +# 配信用の関連アプリをまとめて起動する設定などに使えます。 +# run_with = [] # VAC はオープンソースソフトウェアです。基本的には開発にご協力頂ける方向けのデバッグ出力用のオプションです。 # 通常は設定する必要はありませんが、黒い画面で文字がたくさん流れるのを眺めたい方は TRACE や DEBUG を設定してみてください。 @@ -142,10 +141,7 @@ channel_to = "ai-synth" # ほかにも「です」「ます」→「ですにゃ」「ますにゃ」のような使い方もわりと実用性が高いかもしれません。 # 設定は配列なので複数のファイルを指定できます。もちろん1つから使えます。 # 先に指定してあるほど優先度が高くなります。 -dictionary_files = [ - "dictionary.arknights.txt", - "dictionary.pre-coeiroink.txt", -] +dictionary_files = ["dictionary.arknights.txt", "dictionary.pre-coeiroink.txt"] # true にすると英語の単語をカタカナに変換します。 alkana = true diff --git a/src/conf/mod.rs b/src/conf/mod.rs index 649b2ef..e7acdfb 100644 --- a/src/conf/mod.rs +++ b/src/conf/mod.rs @@ -11,6 +11,18 @@ pub type SharedConf = Arc>; pub const DEFAULT_WEB_UI_ADDRESS: &str = "127.0.0.1:57000"; +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(untagged)] +pub enum RunWith { + Command(String), + CommandIfProcessIsNotRunning { + command: String, + if_not_running: Option, + run_as_admin: Option, + working_dir: Option, + }, +} + #[derive(Debug, Deserialize, Serialize, Clone)] pub struct Conf { pub workers: Option, @@ -26,7 +38,7 @@ pub struct Conf { pub state_data_pretty: Option, #[serde(default)] - pub run_with: Vec, + pub run_with: Vec, pub log_level: Option, @@ -74,6 +86,75 @@ impl Conf { Ok(conf) } + pub fn execute_run_with(&self) -> Result<()> { + use sysinfo::{ProcessExt, ProcessRefreshKind, SystemExt}; + let mut system = sysinfo::System::new(); + system.refresh_processes_specifics(ProcessRefreshKind::everything().without_cpu()); + + for run_with in self.run_with.iter() { + let (command, if_not_running, run_as_admin, working_dir) = match run_with { + RunWith::Command(command) => (command, None, false, None), + RunWith::CommandIfProcessIsNotRunning { + command, + if_not_running, + run_as_admin, + working_dir, + } => ( + command, + if_not_running.as_ref(), + run_as_admin.unwrap_or_default(), + working_dir.as_ref(), + ), + }; + + // if_not_running が指定されている場合は、プロセスが実行中か確認して実行中ならスキップ + if let Some(if_not_running) = if_not_running { + if system + .processes() + .iter() + .any(|(_, process)| process.name().contains(if_not_running)) + { + log::info!( + "run_with: 既に {} を含むプロセスが実行中のため {} の実行はスキップされます。", + if_not_running, + command + ); + continue; + } + } + + // command (引数がある場合も考慮)を実行、またはURLを開く + if command.starts_with("http://") || command.starts_with("https://") { + use webbrowser::{Browser, BrowserOptions}; + log::info!("run_with: {:?} を URL としてブラウザーで開きます。", command); + if let Err(e) = webbrowser::open_browser_with_options(Browser::Default, command, BrowserOptions::new().with_target_hint("vac")) { + log::error!("run_with: URL を開く際にエラーが発生しました: {:?}", e); + } + } else { + let original_dir = match change_working_dir(working_dir) { + Ok(original_dir) => original_dir.map(|p| p.to_string_lossy().to_string()), + Err(e) => { + log::error!("run_with: 作業ディレクトリーの変更に失敗しました: {:?}", e); + continue; + }, + }; + if run_as_admin { + log::warn!("run_with: {:?} を管理者権限で実行を試みます。", command); + if let Err(e) = runas::Command::new(command).status() { + log::error!("run_with: 管理者権限でコマンドを実行する際にエラーが発生しました: {:?}", e); + } + } else { + log::info!("run_with: {:?} をコマンドとして実行します。", command); + if let Err(e) = duct::cmd!(command).start() { + log::error!("run_with: コマンドを実行する際にエラーが発生しました: {:?}", e); + } + } + change_working_dir(original_dir.as_ref())?; + } + } + Ok(()) + } + pub fn to_shared(self) -> SharedConf { Arc::new(RwLock::new(self)) } @@ -93,3 +174,14 @@ impl Conf { fn default_web_ui_resources_path() -> Option { Some("resources".to_string()) } + +/// 現在の作業ディレクトリを変更して、変更前の作業ディレクトリを返します。 +fn change_working_dir(working_dir: Option<&String>) -> Result> { + if let Some(working_dir) = working_dir { + let original_dir = std::env::current_dir()?; + std::env::set_current_dir(working_dir)?; + Ok(Some(original_dir)) + } else { + Ok(None) + } +} diff --git a/src/lib.rs b/src/lib.rs index dbd4a93..559c098 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,6 +46,8 @@ pub async fn run() -> Result<()> { let args = Args::init(audio_sink.clone()).await?; // 設定を読み込みし、ログレベルを更新 let conf = Conf::new(&args)?; + // run_with の実行 + conf.execute_run_with()?; // 共有ステートを作成 let state = State::new(&conf, audio_sink).await?;