1use std::path::{Path, PathBuf};
4
5use clap::{Parser, Subcommand, ValueEnum};
6
7use crate::{codegen, config, logging, service};
8
9#[derive(Parser)]
10#[command(
11 name = "ciplink-server",
12 version,
13 about = "Serves PLC tag values to web apps over a WebSocket"
14)]
15struct Cli {
16 #[command(subcommand)]
17 command: Command,
18}
19
20#[derive(Subcommand)]
21enum Command {
22 Run {
24 #[arg(long, default_value = "tagserver.toml")]
25 config: PathBuf,
26 #[arg(long)]
28 mock: bool,
29 #[arg(long)]
31 log_file: Option<PathBuf>,
32 },
33 Service {
35 #[arg(value_enum)]
36 action: ServiceAction,
37 #[arg(long, default_value = "tagserver.toml")]
38 config: PathBuf,
39 #[arg(long, default_value = "ciplink-server")]
41 name: String,
42 #[arg(long)]
44 log_file: Option<PathBuf>,
45 },
46 DumpTags {
48 #[arg(long, default_value = "tagserver.toml")]
49 config: PathBuf,
50 #[arg(long)]
51 plc: String,
52 #[arg(long)]
53 out: PathBuf,
54 },
55 Types {
57 #[arg(long, default_value = "http://127.0.0.1:8765")]
58 url: String,
59 #[arg(long)]
60 token: String,
61 #[arg(long)]
63 plc: Vec<String>,
64 #[arg(long, value_enum, default_value_t = Format::Tree)]
66 format: Format,
67 #[arg(long)]
68 out: PathBuf,
69 },
70}
71
72#[derive(Clone, Copy, ValueEnum)]
73enum ServiceAction {
74 Install,
75 Remove,
76 Status,
77 Run,
78}
79
80#[derive(Clone, Copy, PartialEq, Eq, ValueEnum)]
81enum Format {
82 Tree,
83 Names,
84}
85
86pub fn main(args: impl IntoIterator<Item = std::ffi::OsString>) -> Result<u8, String> {
88 let exe = std::env::current_exe().map_err(|e| e.to_string())?;
89 main_as(args, &[exe.display().to_string()])
90}
91
92pub fn main_as(
95 args: impl IntoIterator<Item = std::ffi::OsString>,
96 launcher: &[String],
97) -> Result<u8, String> {
98 let cli = match Cli::try_parse_from(args) {
99 Ok(cli) => cli,
100 Err(err) => {
101 let _ = err.print();
102 return Ok(if err.use_stderr() { 2 } else { 0 });
103 }
104 };
105 match cli.command {
106 Command::Run {
107 config,
108 mock,
109 log_file,
110 } => {
111 logging::init(log_file.as_deref())?;
112 crate::run(&config, mock)?;
113 }
114 Command::Service {
115 action,
116 config,
117 name,
118 log_file,
119 } => {
120 let (config, log_file) = service_paths(&config, log_file.as_deref())?;
121 return Ok(match action {
122 ServiceAction::Install => service::install(&name, launcher, &config, &log_file),
123 ServiceAction::Remove => service::remove(&name),
124 ServiceAction::Status => service::status(&name),
125 ServiceAction::Run => {
126 logging::init(Some(&log_file))?;
127 service::supervise(|| crate::run(&config, false));
128 0
129 }
130 });
131 }
132 Command::DumpTags { config, plc, out } => {
133 logging::init(None)?;
134 dump_tags(&config, &plc, &out)?;
135 }
136 Command::Types {
137 url,
138 token,
139 plc,
140 format,
141 out,
142 } => {
143 let plcs: Vec<Option<String>> = if plc.is_empty() {
144 vec![None]
145 } else {
146 plc.into_iter().map(Some).collect()
147 };
148 let bodies = plcs
149 .iter()
150 .map(|p| codegen::fetch_tags(&url, &token, p.as_deref()))
151 .collect::<Result<Vec<_>, _>>()?;
152 let (text, summary) = match format {
153 Format::Tree => {
154 let names: Vec<&str> = bodies
155 .iter()
156 .map(|b| {
157 b.get("plc")
158 .and_then(serde_json::Value::as_str)
159 .unwrap_or("?")
160 })
161 .collect();
162 (
163 codegen::tree_module(&bodies, plcs.len() > 1)?,
164 format!("wrote the tag tree of {}", names.join(", ")),
165 )
166 }
167 Format::Names => {
168 let text = codegen::names_module(&bodies, &plcs);
169 let count = text.matches("\n | ").count();
170 (text, format!("wrote {count} tag names"))
171 }
172 };
173 std::fs::write(&out, text).map_err(|e| format!("{}: {e}", out.display()))?;
174 println!("{summary} to {}", out.display());
175 }
176 }
177 Ok(0)
178}
179
180fn service_paths(config: &Path, log_file: Option<&Path>) -> Result<(PathBuf, PathBuf), String> {
182 let config = std::path::absolute(config).map_err(|e| e.to_string())?;
183 let log_file = match log_file {
184 Some(f) => std::path::absolute(f).map_err(|e| e.to_string())?,
185 None => config.with_file_name("tagserver.log"),
186 };
187 Ok((config, log_file))
188}
189
190fn dump_tags(config_path: &Path, plc: &str, out: &Path) -> Result<(), String> {
192 use crate::plc::{LogixPlc, Plc};
193 let config = config::load_config(config_path)?;
194 let cfg = config
195 .plcs
196 .get(plc)
197 .ok_or_else(|| format!("no PLC {plc} in {}", config_path.display()))?;
198 let mut driver = LogixPlc::new(cfg)?;
199 driver.open()?;
200 let catalog = driver.catalog();
201 driver.close();
202 let tags: serde_json::Map<String, serde_json::Value> = catalog
203 .tags
204 .iter()
205 .map(|(name, node)| {
206 let dims = node.client_dims();
207 let type_ = match dims.as_slice() {
208 [n] => format!("{}[{n}]", node.client_type()),
209 _ => node.client_type().to_owned(),
210 };
211 (name.clone(), serde_json::Value::String(type_))
212 })
213 .collect();
214 let text = serde_json::to_string_pretty(&tags).map_err(|e| e.to_string())?;
215 std::fs::write(out, text).map_err(|e| format!("{}: {e}", out.display()))?;
216 println!("wrote {} tags to {}", tags.len(), out.display());
217 Ok(())
218}
219
220#[cfg(test)]
221#[allow(clippy::unwrap_used)]
222mod tests {
223 use super::*;
224
225 #[test]
228 fn service_run_paths() {
229 let cwd = std::env::current_dir().unwrap();
230 let (config, log_file) = service_paths(Path::new("tagserver.toml"), None).unwrap();
231 assert_eq!(config, cwd.join("tagserver.toml"));
232 assert_eq!(log_file, cwd.join("tagserver.log"));
233 let (_, log_file) =
234 service_paths(Path::new("tagserver.toml"), Some(Path::new("logs/x.log"))).unwrap();
235 assert_eq!(log_file, cwd.join("logs/x.log"));
236 }
237}