1pub mod bridge;
2pub mod cli;
3pub mod config;
4pub mod discord;
5pub mod proxy;
6
7use anyhow::{Context, Result, bail};
8use bridge::ProxyBridge;
9use cli::{BridgeArgs, Cli, Commands, DoctorArgs, LaunchArgs};
10use discord::{DiscordChannel, discover_install, inspect_candidates, is_process_running};
11use proxy::UpstreamProxy;
12
13pub async fn run(cli: Cli) -> Result<()> {
14 let file_config = config::load(cli.config.as_deref())?;
15
16 match cli.command {
17 Commands::Launch(args) => launch(args, file_config).await,
18 Commands::Doctor(args) => doctor(args, file_config),
19 Commands::Bridge(args) => run_bridge(args).await,
20 }
21}
22
23async fn launch(args: LaunchArgs, file_config: config::FileConfig) -> Result<()> {
24 let proxy_text = args
25 .proxy
26 .or(file_config.proxy)
27 .context("missing proxy; pass --proxy or create discord-proxy.toml")?;
28 let upstream = UpstreamProxy::parse(&proxy_text)?;
29
30 let channel = resolve_channel(args.channel, file_config.channel)?;
31 let discord_dir = args.discord_dir.or(file_config.discord_dir);
32 let install = discover_install(channel, discord_dir.as_deref())?;
33
34 let should_bridge = !args.no_bridge && upstream.needs_bridge();
35 let proxy_for_discord = if should_bridge && args.dry_run {
36 planned_bridge_url(args.listen_port)
37 } else if should_bridge {
38 String::new()
39 } else {
40 upstream.command_line_url()
41 };
42
43 if args.no_bridge && upstream.needs_bridge() {
44 if upstream.has_auth() {
45 bail!(
46 "--no-bridge cannot be used with authenticated proxies because credentials are not passed on the Discord command line"
47 );
48 }
49 tracing::warn!("SOCKS proxies usually need the local bridge; --no-bridge may fail");
50 }
51
52 if args.dry_run {
53 let plan = discord::LaunchPlan::new(install, proxy_for_discord);
54 println!("{}", plan.describe());
55 if should_bridge {
56 match args.listen_port {
57 Some(port) => println!("bridge: required for this proxy; port {port} will be used"),
58 None => {
59 println!("bridge: required for this proxy; random port is assigned at runtime")
60 }
61 }
62 }
63 return Ok(());
64 }
65
66 let bridge = if should_bridge {
67 Some(
68 ProxyBridge::start(upstream.clone(), args.listen_port)
69 .await
70 .context("failed to start local proxy bridge")?,
71 )
72 } else {
73 None
74 };
75 let proxy_for_discord = bridge
76 .as_ref()
77 .map(ProxyBridge::local_proxy_url)
78 .unwrap_or(proxy_for_discord);
79 if is_process_running(install.exe_name()) {
80 tracing::warn!(
81 "{} is already running; Discord may reuse the existing process and ignore new proxy arguments",
82 install.exe_name()
83 );
84 }
85 let plan = discord::LaunchPlan::new(install, proxy_for_discord);
86 let mut child = plan.spawn().context("failed to start Discord")?;
87 tracing::info!("Discord launcher process started");
88
89 if let Some(bridge) = bridge {
90 tracing::info!(
91 "local proxy bridge is running at {}; keep this process open while Discord uses the proxy",
92 bridge.local_proxy_url()
93 );
94 tokio::select! {
95 status = child.wait() => {
96 match status {
97 Ok(status) => tracing::info!("Update.exe exited with {status}"),
98 Err(error) => tracing::warn!("failed to wait for Update.exe: {error}"),
99 }
100 tokio::signal::ctrl_c().await.context("failed to wait for Ctrl+C")?;
101 }
102 signal = tokio::signal::ctrl_c() => {
103 signal.context("failed to wait for Ctrl+C")?;
104 }
105 }
106 bridge.shutdown().await?;
107 }
108
109 Ok(())
110}
111
112fn planned_bridge_url(port: Option<u16>) -> String {
113 match port {
114 Some(port) => format!("http://127.0.0.1:{port}"),
115 None => "http://127.0.0.1:<random>".to_string(),
116 }
117}
118
119fn doctor(args: DoctorArgs, file_config: config::FileConfig) -> Result<()> {
120 let channel = resolve_channel(args.channel, file_config.channel)?;
121 let discord_dir = args.discord_dir.or(file_config.discord_dir);
122 let reports = inspect_candidates(channel, discord_dir.as_deref());
123
124 if reports.is_empty() {
125 println!("no candidates found");
126 } else {
127 println!("candidates:");
128 for report in &reports {
129 let status = if report.install().is_some() {
130 "ok"
131 } else {
132 "skip"
133 };
134 let normalized = report
135 .normalized_root()
136 .map(|path| path.display().to_string())
137 .unwrap_or_else(|| "<none>".to_string());
138 let detail = report
139 .install()
140 .map(|install| {
141 format!(
142 "app dir: {}, exe: {}",
143 install.app_dir().display(),
144 install.exe_name()
145 )
146 })
147 .or_else(|| report.error().map(str::to_string))
148 .unwrap_or_else(|| "unknown".to_string());
149
150 println!(
151 "- [{status}] {}: {} -> {} ({detail})",
152 report.source(),
153 report.path().display(),
154 normalized
155 );
156 }
157 }
158
159 let install = discover_install(channel, discord_dir.as_deref())?;
160
161 println!("channel: {}", install.channel());
162 println!("source: {}", install.source());
163 println!("root: {}", install.root().display());
164 println!("app dir: {}", install.app_dir().display());
165 if let Some(update_exe) = install.update_exe() {
166 println!("update exe: {}", update_exe.display());
167 } else {
168 println!("update exe: <direct launch>");
169 }
170 println!("exe path: {}", install.exe_path().display());
171 println!("discord exe: {}", install.exe_name());
172
173 Ok(())
174}
175
176async fn run_bridge(args: BridgeArgs) -> Result<()> {
177 let upstream = UpstreamProxy::parse(&args.proxy)?;
178 let bridge = ProxyBridge::start(upstream, args.listen_port)
179 .await
180 .context("failed to start local proxy bridge")?;
181
182 println!("{}", bridge.local_proxy_url());
183 tracing::info!("local proxy bridge is running; press Ctrl+C to stop");
184 tokio::signal::ctrl_c()
185 .await
186 .context("failed to wait for Ctrl+C")?;
187 bridge.shutdown().await
188}
189
190fn resolve_channel(cli: Option<String>, config: Option<String>) -> Result<DiscordChannel> {
191 let value = cli.or(config).unwrap_or_else(|| "stable".to_string());
192 value.parse().map_err(|error| {
193 let valid = DiscordChannel::valid_values().join(", ");
194 anyhow::anyhow!("{error}; valid channels: {valid}")
195 })
196}
197
198pub fn ensure_proxy_is_supported(upstream: &UpstreamProxy) -> Result<()> {
199 if upstream.host().is_empty() {
200 bail!("proxy host cannot be empty");
201 }
202 Ok(())
203}