use std::{fs, io::IsTerminal, path::PathBuf, process::Command};
use anyhow::{Context, Result, bail};
use inquire::Confirm;
use crate::auth::{config as auth_config, paths};
#[cfg(target_os = "linux")]
const SERVICE_NAME: &str = "gyazo-mcp-server";
pub(crate) fn install() -> Result<()> {
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
if is_installed() {
println!("サービスは既に登録されています。");
println!(" 状態確認: gyazo-mcp-server service status");
println!(" 再登録するには先に 'gyazo-mcp-server service uninstall' を実行してください。");
return Ok(());
}
if paths::has_config_dir_override() {
let persisted = auth_config::read_config_dir_from_default_env();
let override_dir = paths::config_dir()
.map(|d| d.display().to_string())
.unwrap_or_default();
let mismatch = match &persisted {
None => {
Some(format!(
"警告: --config-dir が指定されていますが、永続化されていません。\n\
\x20 現在の override: {override_dir}\n\
\x20 常駐後のサービスはデフォルトの設定ディレクトリを使用します。\n\
\n\
永続化するには:\n\
\x20 gyazo-mcp-server config set config_dir {override_dir}"
))
}
Some(persisted_dir) if persisted_dir != &override_dir => {
Some(format!(
"警告: --config-dir と永続化された config_dir が異なります。\n\
\x20 --config-dir: {override_dir}\n\
\x20 永続化済み: {persisted_dir}\n\
\x20 常駐後のサービスは永続化された方 ({persisted_dir}) を使用します。\n\
\n\
--config-dir の値で上書きするには:\n\
\x20 gyazo-mcp-server config set config_dir {override_dir}"
))
}
_ => None, };
if let Some(message) = mismatch {
eprintln!("{message}");
eprintln!();
if std::io::stdout().is_terminal() {
let proceed = Confirm::new("このままサービスを登録しますか?")
.with_default(false)
.prompt()?;
if !proceed {
println!("中断しました。");
return Ok(());
}
} else {
bail!(
"--config-dir と永続化された config_dir が一致しない状態でのサービス登録は中断されました。"
);
}
}
}
let binary = find_binary()?;
#[cfg(target_os = "linux")]
return install_systemd(&binary);
#[cfg(target_os = "macos")]
return install_launchd(&binary);
#[cfg(target_os = "windows")]
return install_windows_task(&binary);
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
let _ = binary;
bail!(
"この OS ではサービスの自動登録に対応していません。手動でサービス設定を行ってください。"
);
}
}
pub(crate) fn uninstall() -> Result<()> {
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
if !is_installed() {
println!("サービスは登録されていません。");
println!(" 登録するには 'gyazo-mcp-server service install' を実行してください。");
return Ok(());
}
#[cfg(target_os = "linux")]
return uninstall_systemd();
#[cfg(target_os = "macos")]
return uninstall_launchd();
#[cfg(target_os = "windows")]
return uninstall_windows_task();
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
bail!("この OS ではサービスの自動登録に対応していません。手動でサービス設定を行ってください。");
}
pub(crate) fn status() -> Result<()> {
#[cfg(target_os = "linux")]
return status_systemd();
#[cfg(target_os = "macos")]
return status_launchd();
#[cfg(target_os = "windows")]
return status_windows_task();
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
bail!("この OS ではサービスの自動登録に対応していません。手動でサービス設定を行ってください。");
}
pub(crate) fn start(tcp_port: u16) -> Result<()> {
let _ = tcp_port;
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
{
ensure_installed()?;
if is_running(tcp_port) == RunState::Running {
println!("サービスは既に起動しています。");
println!(" 状態確認: gyazo-mcp-server service status");
return Ok(());
}
}
#[cfg(target_os = "linux")]
return start_systemd();
#[cfg(target_os = "macos")]
return start_launchd();
#[cfg(target_os = "windows")]
return start_windows_task();
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
bail!("この OS ではサービスの自動登録に対応していません。手動でサービス設定を行ってください。");
}
pub(crate) fn stop(tcp_port: u16) -> Result<()> {
let _ = tcp_port;
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
{
ensure_installed()?;
if is_running(tcp_port) == RunState::Stopped {
println!("サービスは既に停止しています。");
println!(" 状態確認: gyazo-mcp-server service status");
return Ok(());
}
}
#[cfg(target_os = "linux")]
return stop_systemd();
#[cfg(target_os = "macos")]
return stop_launchd();
#[cfg(target_os = "windows")]
return stop_windows_task(tcp_port);
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
bail!("この OS ではサービスの自動登録に対応していません。手動でサービス設定を行ってください。");
}
pub(crate) fn restart(tcp_port: u16) -> Result<()> {
let _ = tcp_port;
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
{
ensure_installed()?;
if is_running(tcp_port) == RunState::Stopped {
println!("サービスは停止しています。起動のみ実行します。");
}
}
#[cfg(target_os = "linux")]
return restart_systemd();
#[cfg(target_os = "macos")]
return restart_launchd();
#[cfg(target_os = "windows")]
return restart_windows_task(tcp_port);
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
bail!("この OS ではサービスの自動登録に対応していません。手動でサービス設定を行ってください。");
}
fn ensure_installed() -> Result<()> {
if !is_installed() {
bail!("サービスが登録されていません。\n 登録: gyazo-mcp-server service install");
}
Ok(())
}
pub(crate) fn is_installed() -> bool {
is_installed_impl()
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RunState {
Running,
Stopped,
Unknown,
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
fn is_running(tcp_port: u16) -> RunState {
let _ = tcp_port;
is_running_impl(tcp_port)
}
#[cfg(target_os = "linux")]
fn is_running_impl(_tcp_port: u16) -> RunState {
let Ok(status) = Command::new("systemctl")
.args(["--user", "is-active", "--quiet", SERVICE_NAME])
.status()
else {
return RunState::Unknown;
};
match status.code() {
Some(0) => RunState::Running,
Some(3) => RunState::Stopped,
_ => RunState::Unknown,
}
}
#[cfg(target_os = "macos")]
fn is_running_impl(_tcp_port: u16) -> RunState {
let Ok(output) = Command::new("launchctl").arg("list").output() else {
return RunState::Unknown;
};
if !output.status.success() {
return RunState::Unknown;
}
let label = launchd_label();
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines().skip(1) {
let cols: Vec<&str> = line.split('\t').collect();
if cols.len() >= 3 && cols[2] == label {
return if cols[0] != "-" {
RunState::Running
} else {
RunState::Stopped
};
}
}
RunState::Stopped
}
#[cfg(target_os = "windows")]
fn is_running_impl(tcp_port: u16) -> RunState {
let command = format!(
"$conn = Get-NetTCPConnection -LocalPort {tcp_port} -State Listen \
-ErrorAction SilentlyContinue | Select-Object -First 1; \
if (-not $conn) {{ exit 2 }} \
$proc = Get-Process -Id $conn.OwningProcess -ErrorAction SilentlyContinue; \
if ($proc -and $proc.ProcessName -eq 'gyazo-mcp-server') {{ exit 0 }} else {{ exit 2 }}"
);
let Ok(status) = Command::new("powershell")
.args(["-NoProfile", "-Command", &command])
.status()
else {
return RunState::Unknown;
};
match status.code() {
Some(0) => RunState::Running,
Some(2) => RunState::Stopped,
_ => RunState::Unknown,
}
}
#[cfg(target_os = "linux")]
fn is_installed_impl() -> bool {
systemd_unit_path().is_ok_and(|p| p.exists())
}
#[cfg(target_os = "macos")]
fn is_installed_impl() -> bool {
launchd_plist_path().is_ok_and(|p| p.exists())
}
#[cfg(target_os = "windows")]
fn is_installed_impl() -> bool {
Command::new("schtasks")
.args(["/Query", "/TN", task_name()])
.output()
.is_ok_and(|o| o.status.success())
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
fn is_installed_impl() -> bool {
false
}
fn find_binary() -> Result<PathBuf> {
std::env::current_exe().context("実行中のバイナリのパスを取得できませんでした")
}
#[cfg(target_os = "linux")]
fn systemd_unit_dir() -> Result<PathBuf> {
let home = std::env::var("HOME").context("HOME 環境変数が設定されていません")?;
Ok(PathBuf::from(home).join(".config/systemd/user"))
}
#[cfg(target_os = "linux")]
fn systemd_unit_path() -> Result<PathBuf> {
Ok(systemd_unit_dir()?.join(format!("{SERVICE_NAME}.service")))
}
#[cfg(target_os = "linux")]
fn has_systemd_user_manager() -> bool {
Command::new("systemctl")
.args(["--user", "daemon-reload"])
.output()
.is_ok_and(|o| o.status.success())
}
#[cfg(target_os = "linux")]
fn generate_systemd_unit(binary: &std::path::Path) -> String {
let env_file = paths::env_file_path()
.map(|p| format!("EnvironmentFile=-\"{}\"", p.display()))
.unwrap_or_default();
format!(
"[Unit]
Description=Gyazo MCP Server
After=network.target
[Service]
Type=simple
ExecStart=\"{binary}\"
Restart=on-failure
RestartSec=5
{env_file}
[Install]
WantedBy=default.target
",
binary = binary.display(),
)
}
#[cfg(target_os = "linux")]
fn install_systemd(binary: &std::path::Path) -> Result<()> {
if !has_systemd_user_manager() {
bail!(
"systemd の user manager が利用できません。\n\
systemctl --user daemon-reload が失敗しました。\n\
この環境では手動でサービス設定を行ってください。"
);
}
let unit_path = systemd_unit_path()?;
let unit_content = generate_systemd_unit(binary);
fs::create_dir_all(unit_path.parent().unwrap())?;
fs::write(&unit_path, &unit_content).with_context(|| {
format!(
"ユニットファイルを書き込めませんでした: {}",
unit_path.display()
)
})?;
println!("ユニットファイルを作成しました: {}", unit_path.display());
run_command("systemctl", &["--user", "daemon-reload"])?;
run_command("systemctl", &["--user", "enable", SERVICE_NAME])?;
run_command("systemctl", &["--user", "start", SERVICE_NAME])?;
println!("\nサービスを登録・起動しました。");
println!(" 状態確認: gyazo-mcp-server service status");
println!(" ログ確認: journalctl --user -u {SERVICE_NAME} -f");
Ok(())
}
#[cfg(target_os = "linux")]
fn uninstall_systemd() -> Result<()> {
if !has_systemd_user_manager() {
bail!("systemd の user manager が利用できません。");
}
let _ = run_command("systemctl", &["--user", "stop", SERVICE_NAME]);
let _ = run_command("systemctl", &["--user", "disable", SERVICE_NAME]);
let unit_path = systemd_unit_path()?;
if unit_path.exists() {
fs::remove_file(&unit_path).with_context(|| {
format!(
"ユニットファイルを削除できませんでした: {}",
unit_path.display()
)
})?;
println!("ユニットファイルを削除しました: {}", unit_path.display());
}
run_command("systemctl", &["--user", "daemon-reload"])?;
println!("サービス登録を解除しました。");
Ok(())
}
#[cfg(target_os = "linux")]
fn status_systemd() -> Result<()> {
if !has_systemd_user_manager() {
bail!("systemd の user manager が利用できません。");
}
let unit_path = systemd_unit_path()?;
if !unit_path.exists() {
println!("サービスは登録されていません。");
println!(" 登録: gyazo-mcp-server service install");
return Ok(());
}
let output = Command::new("systemctl")
.args(["--user", "status", SERVICE_NAME])
.output()
.context("systemctl status の実行に失敗しました")?;
print!("{}", String::from_utf8_lossy(&output.stdout));
if !output.stderr.is_empty() {
eprint!("{}", String::from_utf8_lossy(&output.stderr));
}
Ok(())
}
#[cfg(target_os = "linux")]
fn start_systemd() -> Result<()> {
ensure_installed()?;
if !has_systemd_user_manager() {
bail!("systemd の user manager が利用できません。");
}
run_command("systemctl", &["--user", "start", SERVICE_NAME])?;
println!("サービスを起動しました。");
Ok(())
}
#[cfg(target_os = "linux")]
fn stop_systemd() -> Result<()> {
ensure_installed()?;
if !has_systemd_user_manager() {
bail!("systemd の user manager が利用できません。");
}
run_command("systemctl", &["--user", "stop", SERVICE_NAME])?;
println!("サービスを停止しました。");
Ok(())
}
#[cfg(target_os = "linux")]
fn restart_systemd() -> Result<()> {
ensure_installed()?;
if !has_systemd_user_manager() {
bail!("systemd の user manager が利用できません。");
}
run_command("systemctl", &["--user", "restart", SERVICE_NAME])?;
println!("サービスを再起動しました。");
Ok(())
}
#[cfg(target_os = "macos")]
fn launchd_plist_dir() -> Result<PathBuf> {
let home = std::env::var("HOME").context("HOME 環境変数が設定されていません")?;
Ok(PathBuf::from(home).join("Library/LaunchAgents"))
}
#[cfg(target_os = "macos")]
fn launchd_label() -> String {
"com.gyazo.mcp-server".to_string()
}
#[cfg(target_os = "macos")]
fn launchd_plist_path() -> Result<PathBuf> {
Ok(launchd_plist_dir()?.join(format!("{}.plist", launchd_label())))
}
#[cfg(target_os = "macos")]
fn generate_launchd_plist(binary: &std::path::Path) -> String {
let label = launchd_label();
let log_dir = paths::config_dir()
.map(|d| d.display().to_string())
.unwrap_or_else(|| "/tmp".to_string());
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>{label}</string>
<key>ProgramArguments</key>
<array>
<string>{binary}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>StandardOutPath</key>
<string>{log_dir}/stdout.log</string>
<key>StandardErrorPath</key>
<string>{log_dir}/stderr.log</string>
</dict>
</plist>
"#,
binary = binary.display(),
)
}
#[cfg(target_os = "macos")]
fn install_launchd(binary: &std::path::Path) -> Result<()> {
let plist_path = launchd_plist_path()?;
let plist_content = generate_launchd_plist(binary);
fs::create_dir_all(plist_path.parent().unwrap())?;
fs::write(&plist_path, &plist_content)
.with_context(|| format!("plist を書き込めませんでした: {}", plist_path.display()))?;
println!("plist を作成しました: {}", plist_path.display());
run_command("launchctl", &["load", &plist_path.display().to_string()])?;
println!("\nサービスを登録・起動しました。");
println!(" 状態確認: gyazo-mcp-server service status");
Ok(())
}
#[cfg(target_os = "macos")]
fn uninstall_launchd() -> Result<()> {
let plist_path = launchd_plist_path()?;
if plist_path.exists() {
let _ = run_command("launchctl", &["unload", &plist_path.display().to_string()]);
fs::remove_file(&plist_path)
.with_context(|| format!("plist を削除できませんでした: {}", plist_path.display()))?;
println!("plist を削除しました: {}", plist_path.display());
}
println!("サービス登録を解除しました。");
Ok(())
}
#[cfg(target_os = "macos")]
fn status_launchd() -> Result<()> {
let plist_path = launchd_plist_path()?;
if !plist_path.exists() {
println!("サービスは登録されていません。");
println!(" 登録: gyazo-mcp-server service install");
return Ok(());
}
let label = launchd_label();
let output = Command::new("launchctl")
.args(["list", &label])
.output()
.context("launchctl list の実行に失敗しました")?;
if output.status.success() {
print!("{}", String::from_utf8_lossy(&output.stdout));
} else {
println!("サービスは登録されていますが、現在実行されていません。");
println!(" plist: {}", plist_path.display());
}
Ok(())
}
#[cfg(target_os = "macos")]
fn start_launchd() -> Result<()> {
ensure_installed()?;
let plist_path = launchd_plist_path()?;
run_command("launchctl", &["load", &plist_path.display().to_string()])?;
println!("サービスを起動しました。");
Ok(())
}
#[cfg(target_os = "macos")]
fn stop_launchd() -> Result<()> {
ensure_installed()?;
let plist_path = launchd_plist_path()?;
run_command("launchctl", &["unload", &plist_path.display().to_string()])?;
println!("サービスを停止しました。");
Ok(())
}
#[cfg(target_os = "macos")]
fn restart_launchd() -> Result<()> {
ensure_installed()?;
let plist_path = launchd_plist_path()?;
let _ = run_command("launchctl", &["unload", &plist_path.display().to_string()]);
run_command("launchctl", &["load", &plist_path.display().to_string()])?;
println!("サービスを再起動しました。");
Ok(())
}
#[cfg(target_os = "windows")]
fn task_name() -> &'static str {
"GyazoMcpServer"
}
#[cfg(target_os = "windows")]
fn scripts_dir() -> Result<PathBuf> {
paths::config_dir().ok_or_else(|| anyhow::anyhow!("設定ディレクトリを特定できませんでした"))
}
#[cfg(target_os = "windows")]
fn ps1_utf8_prelude() -> &'static str {
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\n\
$OutputEncoding = [System.Text.Encoding]::UTF8\n"
}
#[cfg(target_os = "windows")]
fn generate_install_ps1(binary: &std::path::Path) -> String {
let task = task_name();
let prelude = ps1_utf8_prelude();
format!(
r#"{prelude}$action = New-ScheduledTaskAction `
-Execute "powershell.exe" `
-Argument "-WindowStyle Hidden -Command ""Start-Process -WindowStyle Hidden -FilePath '{binary}'"""
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit 0
Register-ScheduledTask -TaskName '{task}' -Action $action -Trigger $trigger -Settings $settings -Description 'Gyazo MCP Server (HTTP transport)'
Write-Host 'タスク "{task}" を登録しました。'
"#,
binary = binary.display(),
)
}
#[cfg(target_os = "windows")]
fn generate_uninstall_ps1() -> String {
let task = task_name();
let prelude = ps1_utf8_prelude();
format!(
r#"{prelude}Unregister-ScheduledTask -TaskName '{task}' -Confirm:$false
Write-Host 'タスク "{task}" を解除しました。'
$listeners = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue
$running = @()
foreach ($conn in $listeners) {{
$proc = Get-Process -Id $conn.OwningProcess -ErrorAction SilentlyContinue
if ($proc -and $proc.ProcessName -eq 'gyazo-mcp-server') {{
$running += [PSCustomObject]@{{ ProcessId = $conn.OwningProcess; LocalPort = $conn.LocalPort }}
}}
}}
$running = $running | Sort-Object ProcessId, LocalPort -Unique
if ($running) {{
Write-Warning '実行中の gyazo-mcp-server (HTTP transport) プロセスが残っています。サービス登録は解除されましたが、これらのプロセスは引き続き動作します。停止が必要な場合は手動で停止してください。'
$running | Format-Table -AutoSize ProcessId, LocalPort | Out-String | Write-Host
Write-Host '停止例: Stop-Process -Id <PID> -Force'
}}
"#,
)
}
#[cfg(target_os = "windows")]
fn run_powershell_script(script_path: &std::path::Path) -> Result<()> {
let output = Command::new("powershell")
.args([
"-ExecutionPolicy",
"Bypass",
"-File",
&script_path.display().to_string(),
])
.output()
.with_context(|| {
format!(
"PowerShell スクリプトの実行に失敗しました: {}",
script_path.display()
)
})?;
print!("{}", String::from_utf8_lossy(&output.stdout));
if !output.stderr.is_empty() {
eprint!("{}", String::from_utf8_lossy(&output.stderr));
}
if !output.status.success() {
bail!(
"PowerShell スクリプトがエラーで終了しました (exit code: {:?})",
output.status.code()
);
}
Ok(())
}
#[cfg(target_os = "windows")]
fn write_ps1_with_bom(path: &std::path::Path, content: &str) -> Result<()> {
let mut bytes = Vec::with_capacity(content.len() + 3);
bytes.extend_from_slice(b"\xEF\xBB\xBF");
bytes.extend_from_slice(content.as_bytes());
fs::write(path, bytes)
.with_context(|| format!("スクリプトを書き込めませんでした: {}", path.display()))
}
#[cfg(target_os = "windows")]
fn install_windows_task(binary: &std::path::Path) -> Result<()> {
let dir = scripts_dir()?;
fs::create_dir_all(&dir)?;
let script_path = dir.join("service-install.ps1");
let script_content = generate_install_ps1(binary);
write_ps1_with_bom(&script_path, &script_content)?;
println!("スクリプトを作成しました: {}", script_path.display());
run_powershell_script(&script_path)?;
println!("\n 状態確認: gyazo-mcp-server service status");
Ok(())
}
#[cfg(target_os = "windows")]
fn uninstall_windows_task() -> Result<()> {
let dir = scripts_dir()?;
fs::create_dir_all(&dir)?;
let script_path = dir.join("service-uninstall.ps1");
let script_content = generate_uninstall_ps1();
write_ps1_with_bom(&script_path, &script_content)?;
run_powershell_script(&script_path)?;
let _ = fs::remove_file(dir.join("service-install.ps1"));
let _ = fs::remove_file(&script_path);
Ok(())
}
#[cfg(target_os = "windows")]
fn run_schtasks(action: &str) -> Result<()> {
let task = task_name();
let command = format!(
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; \
schtasks.exe {action} /TN '{task}'"
);
let output = Command::new("powershell")
.args(["-NoProfile", "-Command", &command])
.output()
.with_context(|| format!("schtasks {action} の実行に失敗しました"))?;
if !output.stdout.is_empty() {
print!("{}", String::from_utf8_lossy(&output.stdout));
}
if !output.stderr.is_empty() {
eprint!("{}", String::from_utf8_lossy(&output.stderr));
}
if !output.status.success() {
bail!(
"schtasks {action} がエラーで終了しました (exit code: {:?})",
output.status.code()
);
}
Ok(())
}
#[cfg(target_os = "windows")]
fn start_windows_task() -> Result<()> {
ensure_installed()?;
run_schtasks("/Run")?;
println!("サービスを起動しました。");
Ok(())
}
#[cfg(target_os = "windows")]
fn build_stop_by_port_command(tcp_port: u16) -> String {
format!(
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; \
$owners = Get-NetTCPConnection -LocalPort {tcp_port} -State Listen \
-ErrorAction SilentlyContinue \
| Select-Object -ExpandProperty OwningProcess -Unique; \
if (-not $owners) {{ \
Write-Host 'ポート {tcp_port} を listen しているプロセスは見つかりませんでした。'; \
exit 0; \
}} \
foreach ($pid_ in $owners) {{ \
$proc = Get-Process -Id $pid_ -ErrorAction SilentlyContinue; \
if ($proc -and $proc.ProcessName -eq 'gyazo-mcp-server') {{ \
Stop-Process -Id $pid_ -Force; \
Write-Host (\"PID $pid_ (gyazo-mcp-server) を停止しました。\"); \
}} else {{ \
$name = if ($proc) {{ $proc.ProcessName }} else {{ 'unknown' }}; \
Write-Error (\"ポート {tcp_port} を listen しているのは gyazo-mcp-server ではありません (PID $pid_, name $name)。停止を中断します。\"); \
exit 1; \
}} \
}}"
)
}
#[cfg(target_os = "windows")]
fn stop_gyazo_mcp_server_by_port(tcp_port: u16) -> Result<()> {
let command = build_stop_by_port_command(tcp_port);
let output = Command::new("powershell")
.args(["-NoProfile", "-Command", &command])
.output()
.context("gyazo-mcp-server プロセスの停止に失敗しました")?;
if !output.stdout.is_empty() {
print!("{}", String::from_utf8_lossy(&output.stdout));
}
if !output.stderr.is_empty() {
eprint!("{}", String::from_utf8_lossy(&output.stderr));
}
if !output.status.success() {
bail!(
"gyazo-mcp-server プロセスの停止がエラーで終了しました (exit code: {:?})",
output.status.code()
);
}
Ok(())
}
#[cfg(target_os = "windows")]
fn stop_windows_task(tcp_port: u16) -> Result<()> {
ensure_installed()?;
stop_gyazo_mcp_server_by_port(tcp_port)?;
println!("サービスを停止しました。");
Ok(())
}
#[cfg(target_os = "windows")]
fn restart_windows_task(tcp_port: u16) -> Result<()> {
ensure_installed()?;
stop_gyazo_mcp_server_by_port(tcp_port)?;
run_schtasks("/Run")?;
println!("サービスを再起動しました。");
Ok(())
}
#[cfg(target_os = "windows")]
fn status_windows_task() -> Result<()> {
let task = task_name();
let command = format!(
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; \
schtasks.exe /Query /TN '{task}' /FO LIST /V"
);
let output = Command::new("powershell")
.args(["-NoProfile", "-Command", &command])
.output()
.context("schtasks の実行に失敗しました")?;
if output.status.success() {
print!("{}", String::from_utf8_lossy(&output.stdout));
} else {
println!("サービスは登録されていません。");
println!(" 登録: gyazo-mcp-server service install");
}
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn run_command(program: &str, args: &[&str]) -> Result<()> {
let output = Command::new(program)
.args(args)
.output()
.with_context(|| format!("{program} の実行に失敗しました"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("{program} がエラーで終了しました: {stderr}");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_binary_returns_current_exe() {
let binary = find_binary().unwrap();
assert!(binary.exists());
}
#[cfg(target_os = "linux")]
#[test]
fn systemd_unit_contains_binary_path() {
let binary = PathBuf::from("/usr/local/bin/gyazo-mcp-server");
let unit = generate_systemd_unit(&binary);
assert!(unit.contains("ExecStart=\"/usr/local/bin/gyazo-mcp-server\""));
assert!(unit.contains("[Install]"));
assert!(unit.contains("WantedBy=default.target"));
}
#[cfg(target_os = "linux")]
#[test]
fn systemd_unit_quotes_paths_with_spaces() {
let binary = PathBuf::from("/opt/my programs/gyazo-mcp-server");
let unit = generate_systemd_unit(&binary);
assert!(unit.contains("ExecStart=\"/opt/my programs/gyazo-mcp-server\""));
}
#[cfg(target_os = "macos")]
#[test]
fn launchd_plist_contains_binary_path() {
let binary = PathBuf::from("/usr/local/bin/gyazo-mcp-server");
let plist = generate_launchd_plist(&binary);
assert!(plist.contains("/usr/local/bin/gyazo-mcp-server"));
assert!(plist.contains("RunAtLoad"));
assert!(plist.contains(&launchd_label()));
}
#[cfg(target_os = "windows")]
#[test]
fn install_ps1_contains_task_name() {
let binary = PathBuf::from(r"C:\Users\test\.cargo\bin\gyazo-mcp-server.exe");
let ps1 = generate_install_ps1(&binary);
assert!(ps1.contains(task_name()));
assert!(ps1.contains(r"C:\Users\test\.cargo\bin\gyazo-mcp-server.exe"));
}
#[cfg(target_os = "windows")]
#[test]
fn stop_by_port_command_uses_owning_process_and_validates_name() {
let command = build_stop_by_port_command(18449);
assert!(
command.contains("Get-NetTCPConnection -LocalPort 18449 -State Listen"),
"ポートで listen プロセスを特定する形式になっていません: {command}"
);
assert!(
command.contains("OwningProcess"),
"OwningProcess から PID を取得していません: {command}"
);
assert!(
command.contains("Stop-Process -Id"),
"PID を指定した Stop-Process になっていません: {command}"
);
assert!(
command.contains("ProcessName -eq 'gyazo-mcp-server'"),
"停止対象が gyazo-mcp-server かどうかを検証していません: {command}"
);
assert!(
!command.contains("schtasks /End"),
"schtasks /End 方式に回帰しています (タスク本体ではなくラッパーしか止まりません): {command}"
);
assert!(
!command.contains("Get-Process -Name gyazo-mcp-server"),
"Get-Process -Name 方式に回帰しています (同名プロセスを巻き込みます): {command}"
);
}
#[cfg(target_os = "windows")]
#[test]
fn stop_by_port_command_embeds_dynamic_port() {
let command = build_stop_by_port_command(19000);
assert!(
command.contains("LocalPort 19000"),
"渡した tcp_port が反映されていません: {command}"
);
}
#[cfg(target_os = "windows")]
#[test]
fn install_ps1_runs_binary_via_hidden_powershell() {
let binary = PathBuf::from(r"C:\bin\gyazo-mcp-server.exe");
let ps1 = generate_install_ps1(&binary);
assert!(
ps1.contains(r#"-Execute "powershell.exe""#),
"powershell.exe を Execute に指定していません"
);
assert!(
ps1.contains("Start-Process -WindowStyle Hidden"),
"Start-Process でバックグラウンド起動にしていません"
);
assert!(
ps1.contains("-WindowStyle Hidden -Command"),
"powershell.exe の WindowStyle が Hidden ではありません"
);
}
#[cfg(target_os = "windows")]
#[test]
fn uninstall_ps1_contains_task_name() {
let ps1 = generate_uninstall_ps1();
assert!(ps1.contains(task_name()));
}
#[cfg(target_os = "windows")]
#[test]
fn generated_ps1_sets_utf8_output_encoding() {
let install = generate_install_ps1(&PathBuf::from(r"C:\bin\gyazo-mcp-server.exe"));
let uninstall = generate_uninstall_ps1();
for (label, ps1) in [("install", &install), ("uninstall", &uninstall)] {
assert!(
ps1.contains("[Console]::OutputEncoding = [System.Text.Encoding]::UTF8"),
"{label} ps1 に Console::OutputEncoding の UTF-8 化が含まれていません"
);
assert!(
ps1.contains("$OutputEncoding = [System.Text.Encoding]::UTF8"),
"{label} ps1 に $OutputEncoding の UTF-8 化が含まれていません"
);
}
}
#[cfg(target_os = "windows")]
#[test]
fn uninstall_ps1_warns_about_running_listener_processes_without_stopping_them() {
let ps1 = generate_uninstall_ps1();
assert!(
ps1.contains("Get-NetTCPConnection -State Listen"),
"listen ポートを起点に走査する形式になっていません: {ps1}"
);
assert!(
ps1.contains("OwningProcess"),
"OwningProcess から PID を辿っていません: {ps1}"
);
assert!(
ps1.contains("ProcessName -eq 'gyazo-mcp-server'"),
"検出対象を gyazo-mcp-server に限定していません: {ps1}"
);
assert!(
ps1.contains("Write-Warning"),
"残存プロセスを警告として通知していません: {ps1}"
);
for line in ps1.lines() {
let trimmed = line.trim_start();
if trimmed.contains("Stop-Process") {
assert!(
trimmed.starts_with("Write-Host"),
"Stop-Process が Write-Host (ヒント表示) 以外の行で使われています \
(自動停止に回帰): {line}"
);
}
}
assert!(
!ps1.contains("Get-Process -Name gyazo-mcp-server"),
"Get-Process -Name 方式に回帰しています: {ps1}"
);
let unregister_pos = ps1
.find("Unregister-ScheduledTask")
.expect("Unregister-ScheduledTask が含まれていません");
let detect_pos = ps1
.find("Get-NetTCPConnection -State Listen")
.expect("検出ブロックが含まれていません");
assert!(
unregister_pos < detect_pos,
"Unregister-ScheduledTask が検出ブロックより後にあります (順序が逆): {ps1}"
);
}
#[cfg(target_os = "windows")]
#[test]
fn write_ps1_with_bom_prepends_utf8_bom() {
let unique = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!("gyazo-mcp-ps1-bom-test-{unique}"));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("test.ps1");
write_ps1_with_bom(&path, "Write-Host 'こんにちは'\n").unwrap();
let bytes = std::fs::read(&path).unwrap();
assert_eq!(&bytes[..3], b"\xEF\xBB\xBF", "UTF-8 BOM が付いていません");
assert_eq!(
std::str::from_utf8(&bytes[3..]).unwrap(),
"Write-Host 'こんにちは'\n"
);
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_dir(&dir);
}
#[test]
fn is_installed_returns_bool_without_panic() {
let _result: bool = is_installed();
}
#[cfg(target_os = "linux")]
#[test]
fn systemd_unit_path_is_under_user_config() {
let path = systemd_unit_path().unwrap();
let path_str = path.display().to_string();
assert!(path_str.contains(".config/systemd/user"));
assert!(path_str.ends_with("gyazo-mcp-server.service"));
}
#[cfg(target_os = "linux")]
#[test]
fn systemd_unit_includes_restart_on_failure() {
let binary = PathBuf::from("/usr/bin/gyazo-mcp-server");
let unit = generate_systemd_unit(&binary);
assert!(unit.contains("Restart=on-failure"));
}
#[cfg(target_os = "macos")]
#[test]
fn launchd_plist_path_is_under_launch_agents() {
let path = launchd_plist_path().unwrap();
let path_str = path.display().to_string();
assert!(path_str.contains("Library/LaunchAgents"));
assert!(path_str.ends_with(".plist"));
}
#[cfg(target_os = "macos")]
#[test]
fn launchd_plist_includes_keep_alive() {
let binary = PathBuf::from("/usr/local/bin/gyazo-mcp-server");
let plist = generate_launchd_plist(&binary);
assert!(plist.contains("KeepAlive"));
}
#[cfg(target_os = "linux")]
#[test]
fn has_systemd_user_manager_returns_bool_without_panic() {
let _result: bool = has_systemd_user_manager();
}
#[cfg(target_os = "linux")]
#[test]
fn systemd_unit_does_not_contain_config_dir_flag() {
let binary = PathBuf::from("/usr/bin/gyazo-mcp-server");
let unit = generate_systemd_unit(&binary);
assert!(
!unit.contains("--config-dir"),
"サービス定義に --config-dir を含めてはならない"
);
}
#[cfg(target_os = "macos")]
#[test]
fn launchd_plist_does_not_contain_config_dir_flag() {
let binary = PathBuf::from("/usr/local/bin/gyazo-mcp-server");
let plist = generate_launchd_plist(&binary);
assert!(
!plist.contains("--config-dir"),
"サービス定義に --config-dir を含めてはならない"
);
}
#[cfg(target_os = "windows")]
#[test]
fn install_ps1_does_not_contain_config_dir_flag() {
let binary = PathBuf::from(r"C:\Users\test\.cargo\bin\gyazo-mcp-server.exe");
let ps1 = generate_install_ps1(&binary);
assert!(
!ps1.contains("--config-dir"),
"サービス定義に --config-dir を含めてはならない"
);
}
}