1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
//! Use and manage Bash shell
//!
//! This is essentially my hello world application and using it as a stalking horse for finding user stories
//! THINK: Considering to change this to a generic unix shell with a plugin for specific type (Bash, ZSH),
//!        since they seem to be common with the differences coming from the scripting and interactive
//!        portions

const APP_NAME: &str = "Bash";
const MODULE_VERSION: &'static str = env!("CARGO_PKG_VERSION");

use anyhow::{Context, Result};
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
use std::process::Command;

// This should likely be enumerated as it will be forked off into a separate project sooner rather than later
use super::*;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bash {
  /// A place to store find_app results
  /// Too many applications exist to enumerate them all, so we want to remember as many as possible
  // HACK: This should be a registry/cache rather than a simple hashmap
  app_cache: HashMap<String, AppInstance>,
  instance: AppInstance,
}

impl Bash {
  fn get_module_version() -> Result<semver::Version> {
    Ok({
      semver::Version::parse(MODULE_VERSION).context(format!(
        "{} has an invalid version number '{}' Cargo.toml",
        APP_NAME, MODULE_VERSION
      ))
    }?)
  }

  fn get_name(&self) -> String {
    match &self.instance.version {
      Some(ver) => format!("{} ({})", APP_NAME, ver),
      None => format!("{} (Unknown Version)", APP_NAME),
    }
  }
}

impl LocalTrait for Bash {
  //
  fn get_local() -> Result<AppInstance> {
    // TODO: Actually fill out the app instance
    Ok(AppInstance::new("bash".to_string()))
  }
}

impl AppTrait for Bash {
  fn get_name(&self) -> String {
    self.get_name()
  }

  fn build(instance: AppInstance, _parent: Option<Rc<dyn ContainerTrait>>) -> Result<Bash> {
    Ok(Bash {
      app_cache: HashMap::new(),
      instance: AppInstance {
        module_version: Some(Bash::get_module_version()?),
        ..instance.clone()
      },
    })
  }

  /// Knows how to get the version number of the installed app (not the module version)
  fn set_version(&self, _instance: AppInstance) -> Result<AppInstance> {
    unimplemented!()
  }
  // /// Figures out how to call the cli using the given container
  // fn set_cli(
  //   &self,
  //   _instance: AppInstance,
  //   _container: Rc<dyn ContainerTrait>,
  // ) -> Result<AppInstance> {
  //   unimplemented!()
  // }
}

impl ContainerTrait for Bash {
  /// This will find a list of apps with configurations that the container knows about
  fn find(&self, query: AppQuery) -> Result<Vec<AppInstance>> {
    match Action::FindApp(FindAppQuery(query)).run(self.clone())? {
      ActionResult::FindApp(result) => Ok(result),
      x => Err(FoundryError::Unreachable).context(format!(
        "Received a non-FindApp Result from Bash::find:\n{:#?}",
        x
      )),
    }
  }

  /// List the known items in the app cache
  fn cached_apps(&self) -> Result<Vec<AppInstance>> {
    unimplemented!("No App Cache for Bash Yet")
  }

  fn forward(&self, _to: AppInstance, _message: Message) -> Result<String> {
    unimplemented!("No ContainerTrait::forward yet")
  }

  /// Get the name/version of the container, usually for use in logging/errors.
  fn get_name(&self) -> String {
    self.get_name()
  }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Action {
  Run(RunOptions),
  FindApp(FindAppQuery),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ActionResult {
  Run(RunResult),
  FindApp(Vec<AppInstance>),
}

// TODO: Make a derive for this
impl Action {
  fn run(&self, target: Bash) -> Result<ActionResult> {
    match self {
      Action::Run(_opts) => unimplemented!("Don't know how to run a command on Bash yet"),
      Action::FindApp(query) => query.run(target.instance),
    }
  }
}

#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct RunOptions {
  pub command: String,
  pub args: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunResult(String);

impl ActionTrait for RunOptions {
  type RESPONSE = ActionResult;

  fn run(&self, _target: AppInstance) -> Result<Self::RESPONSE> {
    let result = Command::new(&self.command).args(&self.args).output();

    // This should be another command based on ActionDefinition
    // TODO: Figure out how errors are returned (stderr vs stdout)
    match result {
      Ok(output) => Ok(ActionResult::Run(RunResult(String::from_utf8(
        output.stdout,
      )?))),
      Err(err) => {
        let msg = format!(
          "Error running the command:\n\tcmd: {:#?}\n\terr: {:#?}",
          self, err,
        );
        log::warn!("{}", msg);
        Err(FoundryError::UnhandledError).context(msg)
      }
    }
  }

  fn to_message(&self, _target: Option<AppInstance>) -> Result<Vec<Message>> {
    let message = Message::Command(Cmd {
      run_as: None,
      command: self.command.clone(),
      args: self.args.clone(),
    });
    // TODO: change this to use target.CliAccess.path instead of bash
    Ok(vec![message])
  }
}

/// Configuration to look up an application in this container
/// TODO: Add a macro to map all the functions to the parent AppQuery
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FindAppQuery(AppQuery);

impl ActionTrait for FindAppQuery {
  type RESPONSE = ActionResult;
  /// Find the first app that matches the conditions of AppDefinition (name, version,  path, etc)
  /// TODO: Convert this to reuse "Run"
  fn run(&self, target: AppInstance) -> Result<Self::RESPONSE> {
    let result = Command::new("bash")
      .args(&["-c", &format!("command -v {}", self.0.name)])
      .output();

    // THis should be another command based on ActionDefinition
    match result {
      Ok(_output) => {
        // TODO: Set the networking/cli in the AppInstance
        let app = AppInstance::new(self.0.name.clone());
        Ok(ActionResult::FindApp(vec![app]))
      }
      Err(_err) => {
        let msg = format!(
          "{} could not find local executable for {}",
          target.full_name(),
          self.0.name,
        );
        log::warn!("{}", msg);
        Err(FoundryError::NotFound).context(msg)
      }
    }
  }

  fn to_message(&self, _target: Option<AppInstance>) -> Result<Vec<Message>> {
    unimplemented!("ActionTrait not implemented for Bash::FindApp::to_string")
  }
}