-
Notifications
You must be signed in to change notification settings - Fork 356
Install to /cache/rayhunter-data for tplink, add --data-dir parameter #886
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,9 @@ | ||
| use std::future::Future; | ||
| use std::net::SocketAddr; | ||
|
|
||
| use anyhow::Result; | ||
| use anyhow::{Result, bail}; | ||
|
|
||
| use crate::output::println; | ||
| use crate::output::{print, println}; | ||
|
|
||
| /// Abstraction for device communication (telnet or ADB) | ||
| pub trait DeviceConnection { | ||
|
|
@@ -17,19 +17,20 @@ pub trait DeviceConnection { | |
|
|
||
| /// Check if a file exists using a DeviceConnection | ||
| pub async fn file_exists<C: DeviceConnection>(conn: &mut C, path: &str) -> bool { | ||
| conn.run_command(&format!("test -f {path} && echo exists || echo missing")) | ||
| conn.run_command(&format!("test -f '{path}' && echo exists || echo missing")) | ||
| .await | ||
| .map(|output| output.contains("exists")) | ||
| .unwrap_or(false) | ||
| } | ||
|
|
||
| /// Shared config installation logic | ||
| /// Shared config installation logic. Installs to /data/rayhunter/config.toml which resolves | ||
| /// through the symlink to the actual data directory. | ||
| pub async fn install_config<C: DeviceConnection>( | ||
| conn: &mut C, | ||
| config_path: &str, | ||
| device_type: &str, | ||
| reset_config: bool, | ||
| ) -> Result<()> { | ||
| let config_path = "/data/rayhunter/config.toml"; | ||
| if reset_config || !file_exists(conn, config_path).await { | ||
| let config = crate::CONFIG_TOML.replace( | ||
| r#"#device = "orbic""#, | ||
|
|
@@ -42,6 +43,118 @@ pub async fn install_config<C: DeviceConnection>( | |
| Ok(()) | ||
| } | ||
|
|
||
| /// Check if a directory exists using a DeviceConnection | ||
| pub async fn dir_exists<C: DeviceConnection>(conn: &mut C, path: &str) -> bool { | ||
| conn.run_command(&format!("test -d '{path}' && echo exists || echo missing")) | ||
| .await | ||
| .map(|output| output.contains("exists")) | ||
| .unwrap_or(false) | ||
| } | ||
|
|
||
| /// Check if a path is a symlink using a DeviceConnection | ||
| pub async fn is_symlink<C: DeviceConnection>(conn: &mut C, path: &str) -> bool { | ||
| conn.run_command(&format!("test -L '{path}' && echo yes || echo no")) | ||
| .await | ||
| .map(|output| output.contains("yes")) | ||
| .unwrap_or(false) | ||
| } | ||
|
|
||
| /// Read the target of a symlink using a DeviceConnection | ||
| pub async fn readlink<C: DeviceConnection>(conn: &mut C, path: &str) -> Result<String> { | ||
| // Use a prefix marker to find the actual output line, since some shells (TP-Link) echo | ||
| // back the command and run_command appends protocol lines. | ||
| let output = conn | ||
| .run_command(&format!("echo RL:$(readlink '{path}')")) | ||
| .await?; | ||
|
|
||
| for line in output.lines() { | ||
| if let Some(target) = line.trim().strip_prefix("RL:") { | ||
| return Ok(target.to_string()); | ||
| } | ||
| } | ||
|
|
||
| bail!("unexpected readlink output: {output:?}"); | ||
| } | ||
|
|
||
| /// Set up the data directory at `data_dir` and create a symlink from `/data/rayhunter` to it. | ||
| /// | ||
| /// Handles migration from old locations: | ||
| /// - If `/data/rayhunter` is a real directory, moves its contents to `data_dir` | ||
| /// - If `/data/rayhunter` is a symlink to a different location, moves from the old target | ||
| /// - If `/data/rayhunter` doesn't exist, just creates the symlink | ||
| /// - If `/data/rayhunter` is a symlink to `data_dir`, does nothing | ||
| pub async fn setup_data_directory<C: DeviceConnection>(conn: &mut C, data_dir: &str) -> Result<()> { | ||
| if data_dir == "/data/rayhunter" { | ||
| bail!("data_dir must not be /data/rayhunter"); | ||
| } | ||
|
|
||
| if data_dir.contains("'") { | ||
| bail!("data_dir must not contain an apostrophe (')"); | ||
| } | ||
|
|
||
| // Determine where old data lives, if anywhere | ||
| let old_data_source = if is_symlink(conn, "/data/rayhunter").await { | ||
| let current_target = readlink(conn, "/data/rayhunter").await?; | ||
| if current_target == data_dir { | ||
| println!("Data directory already configured at {data_dir}"); | ||
| return Ok(()); | ||
| } | ||
| conn.run_command("rm -f /data/rayhunter").await?; | ||
| // The old symlink target is where data actually lives | ||
| if dir_exists(conn, ¤t_target).await { | ||
| Some(current_target) | ||
| } else { | ||
| None | ||
| } | ||
| } else if dir_exists(conn, "/data/rayhunter").await { | ||
| if dir_exists(conn, data_dir).await { | ||
| bail!("Both /data/rayhunter and {data_dir} exist and are directories."); | ||
| } | ||
| // Real directory (pre-migration Orbic state) | ||
| Some("/data/rayhunter".to_string()) | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| // Migrate old data if present | ||
| if let Some(old_source) = &old_data_source { | ||
| // Stop rayhunter-daemon so it doesn't write during migration. | ||
| // The device will be rebooted at the end of installation anyway. | ||
| print!("Stopping rayhunter-daemon ... "); | ||
| let _ = conn | ||
| .run_command("/etc/init.d/rayhunter_daemon stop 2>/dev/null; true") | ||
| .await; | ||
| println!("ok"); | ||
|
|
||
| print!("Migrating data from {old_source} to {data_dir} ... "); | ||
|
|
||
| // mv old data into its place. If source and destination are on the same filesystem, | ||
| // this is an instant rename. | ||
| // XXX: DeviceConnection::run_command does not expose the exit code of the ran command. It | ||
| // probably should, or a utility for it should exist? | ||
| let mv_output = conn | ||
| .run_command(&format!("mv {old_source} '{data_dir}' && echo MV_OK")) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Single quote old_source |
||
| .await?; | ||
| if mv_output.contains("MV_OK") { | ||
| println!("ok"); | ||
| } else { | ||
| bail!("Failed to move data from {old_source} to {data_dir}:\n{mv_output}"); | ||
| } | ||
| } else { | ||
| // No migration needed, just ensure the target directory exists | ||
| conn.run_command(&format!("mkdir -p '{data_dir}'")).await?; | ||
| } | ||
|
|
||
| // Create the symlink | ||
| print!("Creating symlink /data/rayhunter -> {data_dir} ... "); | ||
| conn.run_command("mkdir -p /data").await?; | ||
| conn.run_command(&format!("ln -sf '{data_dir}' /data/rayhunter")) | ||
| .await?; | ||
| println!("ok"); | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| /// Telnet-based connection wrapper | ||
| pub struct TelnetConnection { | ||
| pub addr: SocketAddr, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ use env_logger::Env; | |
| use anyhow::bail; | ||
|
|
||
| mod connection; | ||
| mod moxee; | ||
| #[cfg(not(target_os = "android"))] | ||
| mod orbic; | ||
| mod orbic_auth; | ||
|
|
@@ -40,9 +41,11 @@ enum Command { | |
| /// Install rayhunter on the Orbic RC400L using the legacy USB+ADB-based installer. | ||
| #[cfg(not(target_os = "android"))] | ||
| OrbicUsb(InstallOrbic), | ||
| /// Install rayhunter on the Orbic RC400L or Moxee Hotspot via network. | ||
| /// Install rayhunter on the Orbic RC400L via network. | ||
| #[clap(alias = "orbic-network")] | ||
| Orbic(OrbicNetworkArgs), | ||
| /// Install rayhunter on the Moxee Hotspot via network. | ||
| Moxee(MoxeeArgs), | ||
| /// Install rayhunter on the TMobile TMOHS1. | ||
| Tmobile(TmobileArgs), | ||
| /// Install rayhunter on the Uz801. | ||
|
|
@@ -84,6 +87,12 @@ struct InstallTpLink { | |
| /// Overwrite config.toml even if it already exists on the device. | ||
| #[arg(long)] | ||
| reset_config: bool, | ||
|
|
||
| /// Override the data directory path. Defaults to /cache/rayhunter-data (or SD card path when | ||
| /// SD card is used). Must not be /data/rayhunter, which lives on a storage partition that's | ||
| /// too small for normal Rayhunter operation. | ||
| #[arg(long)] | ||
| data_dir: Option<String>, | ||
| } | ||
|
|
||
| #[derive(Parser, Debug)] | ||
|
|
@@ -110,6 +119,35 @@ struct OrbicNetworkArgs { | |
| /// Overwrite config.toml even if it already exists on the device. | ||
| #[arg(long)] | ||
| reset_config: bool, | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this right? why are we migrating data_dir by default on Orbics?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it was easier to make /data/rayhunter a symlink to some other directory in all cases than to special-case the orbic where /data/rayhunter is currently a dir. that's why the layout for orbic would end up with a symlink from /data/rayhunter to /data/rayhunter-data even though it's not really necessary |
||
| /// Override the data directory path. Defaults to /data/rayhunter-data. | ||
| /// Must not be /data/rayhunter. | ||
| #[arg(long)] | ||
| data_dir: Option<String>, | ||
| } | ||
|
|
||
| #[derive(Parser, Debug)] | ||
| struct MoxeeArgs { | ||
| /// IP address for Moxee admin interface, if custom. | ||
| #[arg(long, default_value = "192.168.1.1")] | ||
| admin_ip: String, | ||
|
|
||
| /// Admin username for authentication. | ||
| #[arg(long, default_value = "admin")] | ||
| admin_username: String, | ||
|
|
||
| /// Admin password for authentication. | ||
| #[arg(long)] | ||
| admin_password: Option<String>, | ||
|
|
||
| /// Overwrite config.toml even if it already exists on the device. | ||
| #[arg(long)] | ||
| reset_config: bool, | ||
|
|
||
| /// Override the data directory path. Defaults to /cache/rayhunter-data. | ||
| /// Must not be /data/rayhunter. | ||
| #[arg(long)] | ||
| data_dir: Option<String>, | ||
| } | ||
|
|
||
| #[derive(Parser, Debug)] | ||
|
|
@@ -245,7 +283,8 @@ async fn run(args: Args) -> Result<(), Error> { | |
| .context("Failed to install rayhunter on the Pinephone's Quectel modem")?, | ||
| #[cfg(not(target_os = "android"))] | ||
| Command::OrbicUsb(args) => orbic::install(args.reset_config).await.context("\nFailed to install rayhunter on the Orbic RC400L (USB installer)")?, | ||
| Command::Orbic(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password, args.reset_config).await.context("\nFailed to install rayhunter on the Orbic RC400L")?, | ||
| Command::Orbic(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password, args.reset_config, args.data_dir).await.context("\nFailed to install rayhunter on the Orbic RC400L")?, | ||
| Command::Moxee(args) => moxee::install(args).await.context("\nFailed to install rayhunter on the Moxee Hotspot")?, | ||
| Command::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?, | ||
| Command::Util(subcommand) => { | ||
| match subcommand.command { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| use anyhow::Result; | ||
|
|
||
| use crate::MoxeeArgs; | ||
|
|
||
| pub async fn install(args: MoxeeArgs) -> Result<()> { | ||
| let data_dir = args.data_dir.or(Some("/cache/rayhunter-data".to_string())); | ||
| crate::orbic_network::install( | ||
| args.admin_ip, | ||
| args.admin_username, | ||
| args.admin_password, | ||
| args.reset_config, | ||
| data_dir, | ||
| ) | ||
| .await | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,7 @@ use serde::Deserialize; | |
| use tokio::time::sleep; | ||
|
|
||
| use crate::RAYHUNTER_DAEMON_INIT; | ||
| use crate::connection::{TelnetConnection, install_config}; | ||
| use crate::connection::{TelnetConnection, install_config, setup_data_directory}; | ||
| use crate::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password}; | ||
| use crate::output::{eprintln, print, println}; | ||
| use crate::util::{interactive_shell, telnet_send_command, telnet_send_file}; | ||
|
|
@@ -147,6 +147,7 @@ pub async fn install( | |
| admin_username: String, | ||
| admin_password: Option<String>, | ||
| reset_config: bool, | ||
| data_dir: Option<String>, | ||
| ) -> Result<()> { | ||
| let Some(admin_password) = admin_password else { | ||
| eprintln!( | ||
|
|
@@ -170,7 +171,8 @@ pub async fn install( | |
| wait_for_telnet(&admin_ip).await?; | ||
| println!("done"); | ||
|
|
||
| setup_rayhunter(&admin_ip, reset_config).await | ||
| let data_dir = data_dir.unwrap_or_else(|| "/data/rayhunter-data".to_string()); | ||
| setup_rayhunter(&admin_ip, reset_config, &data_dir).await | ||
| } | ||
|
|
||
| async fn wait_for_telnet(admin_ip: &str) -> Result<()> { | ||
|
|
@@ -194,7 +196,7 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> { | |
| Ok(()) | ||
| } | ||
|
|
||
| async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> { | ||
| async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) -> Result<()> { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i feel like this should be an optional arg, and we don't call
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #886 (comment) -- i've made /data/rayhunter a symlink always, so that the code becomes simpler. orbic is a kind of outlier between these devices as it is the only device where /data/ is the partition where we want rayhunter installed. |
||
| let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?; | ||
| let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); | ||
|
|
||
|
|
@@ -208,7 +210,8 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> { | |
| ) | ||
| .await?; | ||
|
|
||
| telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0", false).await?; | ||
| let mut conn = TelnetConnection::new(addr, false); | ||
| setup_data_directory(&mut conn, data_dir).await?; | ||
|
|
||
| telnet_send_file( | ||
| addr, | ||
|
|
@@ -218,14 +221,7 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> { | |
| ) | ||
| .await?; | ||
|
|
||
| let mut conn = TelnetConnection::new(addr, false); | ||
| install_config( | ||
| &mut conn, | ||
| "/data/rayhunter/config.toml", | ||
| "orbic", | ||
| reset_config, | ||
| ) | ||
| .await?; | ||
| install_config(&mut conn, "orbic", reset_config).await?; | ||
|
|
||
| telnet_send_file( | ||
| addr, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it seems this only results in a new directory at
data_dirif there was old data that needed migrating. should it not do amkdir -p {data_dir}otherwise?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't that's what's on line 139?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if there was old data, moving that data to the new location ensures the directory exists
if there was no old data, like 139-140 handles this case.