<commit_before>/// Some code copied from the Cargo Project at Commit:
/// https://github.com/rust-lang/cargo/commit/def249f9c18280d84f29fd96978389689fb61051

use errors::*;
use git2;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use url::Url;

const FETCH_HEAD: &'static str = "FETCH_HEAD";

pub struct GitRepo {
    repository: git2::Repository,
    path: PathBuf,
    remote: Url,
    revspec: String,
}

impl GitRepo {
    pub fn with_remote<P: AsRef<Path>>(path: P, remote: Url, revspec: String) -> Result<GitRepo> {
        let path = path.as_ref();

        let (repository, new) = if !path.is_dir() {
            trace!("Initializing git repo {}", path.display());
            fs::create_dir_all(path)?;
            (git2::Repository::init(path)?, true)
        } else {
            trace!("Opening git repo {}", path.display());
            (git2::Repository::open(path)?, false)
        };

        let git_repo = GitRepo {
            repository: repository,
            path: path.to_owned(),
            remote: remote,
            revspec: revspec,
        };

        if new {
            git_repo.update()?;
        }

        Ok(git_repo)
    }

    pub fn path(&self) -> &Path {
        &self.path
    }

    /// Prepare the authentication callbacks for cloning a git repository.
    ///
    /// The main purpose of this function is to construct the "authentication
    /// callback" which is used to clone a repository. This callback will attempt to
    /// find the right authentication on the system (without user input) and will
    /// guide libgit2 in doing so.
    ///
    /// The callback is provided `allowed` types of credentials, and we try to do as
    /// much as possible based on that:
    ///
    /// * Prioritize SSH keys from the local ssh agent as they're likely the most
    ///   reliable. The username here is prioritized from the credential
    ///   callback, then from whatever is configured in git itself, and finally
    ///   we fall back to the generic user of `git`.
    ///
    /// * If a username/password is allowed, then we fallback to git2-rs's
    ///   implementation of the credential helper. This is what is configured
    ///   with `credential.helper` in git, and is the interface for the OSX
    ///   keychain, for example.
    ///
    /// * After the above two have failed, we just kinda grapple attempting to
    ///   return *something*.
    ///
    /// If any form of authentication fails, libgit2 will repeatedly ask us for
    /// credentials until we give it a reason to not do so. To ensure we don't
    /// just sit here looping forever we keep track of authentications we've
    /// attempted and we don't try the same ones again.
    fn with_authentication<T, F>(&self, cfg: &git2::Config, mut f: F) -> Result<T>
        where F: FnMut(&mut git2::Credentials) -> Result<T>
    {
        let url = self.remote.as_str();

        let mut cred_helper = git2::CredentialHelper::new(url);
        cred_helper.config(cfg);

        let mut ssh_username_requested = false;
        let mut cred_helper_bad = None;
        let mut ssh_agent_attempts = Vec::new();
        let mut any_attempts = false;
        let mut tried_sshkey = false;

        let mut res = f(&mut |url, username, allowed| {
            any_attempts = true;
            // libgit2's "USERNAME" authentication actually means that it's just
            // asking us for a username to keep going. This is currently only really
            // used for SSH authentication and isn't really an authentication type.
            // The logic currently looks like:
            //
            //      let user = ...;
            //      if (user.is_null())
            //          user = callback(USERNAME, null, ...);
            //
            //      callback(SSH_KEY, user, ...)
            //
            // So if we're being called here then we know that (a) we're using ssh
            // authentication and (b) no username was specified in the URL that
            // we're trying to clone. We need to guess an appropriate username here,
            // but that may involve a few attempts. Unfortunately we can't switch
            // usernames during one authentication session with libgit2, so to
            // handle this we bail out of this authentication session after setting
            // the flag `ssh_username_requested`, and then we handle this below.
            if allowed.contains(git2::USERNAME) {
                debug_assert!(username.is_none());
                ssh_username_requested = true;
                return Err(git2::Error::from_str("gonna try usernames later"));
            }

            // An "SSH_KEY" authentication indicates that we need some sort of SSH
            // authentication. This can currently either come from the ssh-agent
            // process or from a raw in-memory SSH key. Cargo only supports using
            // ssh-agent currently.
            //
            // If we get called with this then the only way that should be possible
            // is if a username is specified in the URL itself (e.g. `username` is
            // Some), hence the unwrap() here. We try custom usernames down below.
            if allowed.contains(git2::SSH_KEY) && !tried_sshkey {
                // If ssh-agent authentication fails, libgit2 will keep
                // calling this callback asking for other authentication
                // methods to try. Make sure we only try ssh-agent once,
                // to avoid looping forever.
                tried_sshkey = true;
                let username = username.unwrap();
                debug_assert!(!ssh_username_requested);
                ssh_agent_attempts.push(username.to_string());
                return git2::Cred::ssh_key_from_agent(username);
            }

            // Sometimes libgit2 will ask for a username/password in plaintext. This
            // is where Cargo would have an interactive prompt if we supported it,
            // but we currently don't! Right now the only way we support fetching a
            // plaintext password is through the `credential.helper` support, so
            // fetch that here.
            if allowed.contains(git2::USER_PASS_PLAINTEXT) {
                let r = git2::Cred::credential_helper(cfg, url, username);
                cred_helper_bad = Some(r.is_err());
                return r;
            }

            // I'm... not sure what the DEFAULT kind of authentication is, but seems
            // easy to support?
            if allowed.contains(git2::DEFAULT) {
                return git2::Cred::default();
            }

            // Whelp, we tried our best
            Err(git2::Error::from_str("no authentication available"))
        });

        // Ok, so if it looks like we're going to be doing ssh authentication, we
        // want to try a few different usernames as one wasn't specified in the URL
        // for us to use. In order, we'll try:
        //
        // * A credential helper's username for this URL, if available.
        // * This account's username.
        // * "git"
        //
        // We have to restart the authentication session each time (due to
        // constraints in libssh2 I guess? maybe this is inherent to ssh?), so we
        // call our callback, `f`, in a loop here.
        if ssh_username_requested {
            debug_assert!(res.is_err());
            let mut attempts = Vec::new();
            attempts.push("git".to_string());
            if let Ok(s) = env::var("USER").or_else(|_| env::var("USERNAME")) {
                attempts.push(s);
            }
            if let Some(ref s) = cred_helper.username {
                attempts.push(s.clone());
            }

            while let Some(s) = attempts.pop() {
                // We should get `USERNAME` first, where we just return our attempt,
                // and then after that we should get `SSH_KEY`. If the first attempt
                // fails we'll get called again, but we don't have another option so
                // we bail out.
                let mut attempts = 0;

                res = f(&mut |_url, username, allowed| {
                    if allowed.contains(git2::USERNAME) {
                        return git2::Cred::username(&s);
                    }
                    if allowed.contains(git2::SSH_KEY) {
                        debug_assert_eq!(Some(&s[..]), username);
                        attempts += 1;
                        if attempts == 1 {
                            ssh_agent_attempts.push(s.to_string());
                            return git2::Cred::ssh_key_from_agent(&s);
                        }
                    }
                    Err(git2::Error::from_str("no authentication available"))
                });

                // If we made two attempts then that means:
                //
                // 1. A username was requested, we returned `s`.
                // 2. An ssh key was requested, we returned to look up `s` in the
                //    ssh agent.
                // 3. For whatever reason that lookup failed, so we were asked again
                //    for another mode of authentication.
                //
                // Essentially, if `attempts == 2` then in theory the only error was
                // that this username failed to authenticate (e.g. no other network
                // errors happened). Otherwise something else is funny so we bail
                // out.
                if attempts != 2 {
                    break;
                }
            }
        }

        if res.is_ok() || !any_attempts {
            return res.map_err(From::from);
        }

        // In the case of an authentication failure (where we tried something) then
        // we try to give a more helpful error message about precisely what we
        // tried.
        res.map_err(Error::from).chain_err(|| {
            let mut msg = "failed to authenticate when downloading repository".to_string();

            if !ssh_agent_attempts.is_empty() {
                let names = ssh_agent_attempts.iter()
                    .map(|s| format!("`{}`", s))
                    .collect::<Vec<_>>()
                    .join(", ");
                msg.push_str(&format!("\nattempted ssh-agent authentication, but none of the \
                                       usernames {} succeeded",
                                      names));
            }

            if let Some(failed_cred_helper) = cred_helper_bad {
                if failed_cred_helper {
                    msg.push_str("\nattempted to find username/password via git's \
                                  `credential.helper` support, but failed");
                } else {
                    msg.push_str("\nattempted to find username/password via `credential.helper`, \
                                  but maybe the found credentials were incorrect");
                }
            }

            msg
        })
    }

    pub fn reset(&self, revspec: &str) -> Result<()> {
        let oid = {
            let obj = self.repository.revparse_single(revspec)?;

            match obj.as_tag() {
                Some(tag) => tag.target_id(),
                None => obj.id(),
            }
        };

        let object = self.repository.find_object(oid, None)?;
        self.repository.reset(&object, git2::ResetType::Hard, None)?;
        Ok(())
    }

    pub fn update(&self) -> Result<()> {
        info!("Updating {}", self.remote);

        self.with_authentication(&self.repository.config()?, |f| {
                let mut cb = git2::RemoteCallbacks::new();
                cb.credentials(f);

                let url = self.remote.as_str();

                // Create a local anonymous remote in the repository to fetch the url
                let mut remote = self.repository.remote_anonymous(url)?;
                let mut opts = git2::FetchOptions::new();

                opts.remote_callbacks(cb)
                    .download_tags(git2::AutotagOption::All);

                remote.fetch(&[self.revspec.as_str()], Some(&mut opts), None)?;
                Ok(())
            })?;

        self.reset(FETCH_HEAD)
    }
}
<commit_msg>[repository] format a bit<commit_after>/// Some code copied from the Cargo Project at Commit:
/// https://github.com/rust-lang/cargo/commit/def249f9c18280d84f29fd96978389689fb61051

use errors::*;
use git2;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use url::Url;

const FETCH_HEAD: &'static str = "FETCH_HEAD";

pub struct GitRepo {
    repository: git2::Repository,
    path: PathBuf,
    remote: Url,
    revspec: String,
}

impl GitRepo {
    pub fn with_remote<P: AsRef<Path>>(path: P, remote: Url, revspec: String) -> Result<GitRepo> {
        let path = path.as_ref();

        let (repository, new) = if !path.is_dir() {
            trace!("Initializing git repo {}", path.display());
            fs::create_dir_all(path)?;
            (git2::Repository::init(path)?, true)
        } else {
            trace!("Opening git repo {}", path.display());
            (git2::Repository::open(path)?, false)
        };

        let git_repo = GitRepo {
            repository: repository,
            path: path.to_owned(),
            remote: remote,
            revspec: revspec,
        };

        if new {
            git_repo.update()?;
        }

        Ok(git_repo)
    }

    pub fn path(&self) -> &Path {
        &self.path
    }

    /// Prepare the authentication callbacks for cloning a git repository.
    ///
    /// The main purpose of this function is to construct the "authentication
    /// callback" which is used to clone a repository. This callback will attempt to
    /// find the right authentication on the system (without user input) and will
    /// guide libgit2 in doing so.
    ///
    /// The callback is provided `allowed` types of credentials, and we try to do as
    /// much as possible based on that:
    ///
    /// * Prioritize SSH keys from the local ssh agent as they're likely the most
    ///   reliable. The username here is prioritized from the credential
    ///   callback, then from whatever is configured in git itself, and finally
    ///   we fall back to the generic user of `git`.
    ///
    /// * If a username/password is allowed, then we fallback to git2-rs's
    ///   implementation of the credential helper. This is what is configured
    ///   with `credential.helper` in git, and is the interface for the OSX
    ///   keychain, for example.
    ///
    /// * After the above two have failed, we just kinda grapple attempting to
    ///   return *something*.
    ///
    /// If any form of authentication fails, libgit2 will repeatedly ask us for
    /// credentials until we give it a reason to not do so. To ensure we don't
    /// just sit here looping forever we keep track of authentications we've
    /// attempted and we don't try the same ones again.
    fn with_authentication<T, F>(&self, cfg: &git2::Config, mut callback: F) -> Result<T>
    where
        F: FnMut(&mut git2::Credentials) -> Result<T>,
    {
        let url = self.remote.as_str();

        let mut cred_helper = git2::CredentialHelper::new(url);
        cred_helper.config(cfg);

        let mut ssh_username_requested = false;
        let mut cred_helper_bad = None;
        let mut ssh_agent_attempts = Vec::new();
        let mut any_attempts = false;
        let mut tried_sshkey = false;

        let mut res = callback(&mut |url, username, allowed| {
            any_attempts = true;

            // libgit2's "USERNAME" authentication actually means that it's just
            // asking us for a username to keep going. This is currently only really
            // used for SSH authentication and isn't really an authentication type.
            // The logic currently looks like:
            //
            //      let user = ...;
            //      if (user.is_null())
            //          user = callback(USERNAME, null, ...);
            //
            //      callback(SSH_KEY, user, ...)
            //
            // So if we're being called here then we know that (a) we're using ssh
            // authentication and (b) no username was specified in the URL that
            // we're trying to clone. We need to guess an appropriate username here,
            // but that may involve a few attempts. Unfortunately we can't switch
            // usernames during one authentication session with libgit2, so to
            // handle this we bail out of this authentication session after setting
            // the flag `ssh_username_requested`, and then we handle this below.
            if allowed.contains(git2::USERNAME) {
                debug_assert!(username.is_none());
                ssh_username_requested = true;
                return Err(git2::Error::from_str("gonna try usernames later"));
            }

            // An "SSH_KEY" authentication indicates that we need some sort of SSH
            // authentication. This can currently either come from the ssh-agent
            // process or from a raw in-memory SSH key. Cargo only supports using
            // ssh-agent currently.
            //
            // If we get called with this then the only way that should be possible
            // is if a username is specified in the URL itself (e.g. `username` is
            // Some), hence the unwrap() here. We try custom usernames down below.
            if allowed.contains(git2::SSH_KEY) && !tried_sshkey {
                // If ssh-agent authentication fails, libgit2 will keep
                // calling this callback asking for other authentication
                // methods to try. Make sure we only try ssh-agent once,
                // to avoid looping forever.
                tried_sshkey = true;
                let username = username.unwrap();
                debug_assert!(!ssh_username_requested);
                ssh_agent_attempts.push(username.to_string());
                return git2::Cred::ssh_key_from_agent(username);
            }

            // Sometimes libgit2 will ask for a username/password in plaintext. This
            // is where Cargo would have an interactive prompt if we supported it,
            // but we currently don't! Right now the only way we support fetching a
            // plaintext password is through the `credential.helper` support, so
            // fetch that here.
            if allowed.contains(git2::USER_PASS_PLAINTEXT) {
                let r = git2::Cred::credential_helper(cfg, url, username);
                cred_helper_bad = Some(r.is_err());
                return r;
            }

            // I'm... not sure what the DEFAULT kind of authentication is, but seems
            // easy to support?
            if allowed.contains(git2::DEFAULT) {
                return git2::Cred::default();
            }

            // Whelp, we tried our best
            Err(git2::Error::from_str("no authentication available"))
        });

        // Ok, so if it looks like we're going to be doing ssh authentication, we
        // want to try a few different usernames as one wasn't specified in the URL
        // for us to use. In order, we'll try:
        //
        // * A credential helper's username for this URL, if available.
        // * This account's username.
        // * "git"
        //
        // We have to restart the authentication session each time (due to
        // constraints in libssh2 I guess? maybe this is inherent to ssh?), so we
        // call our callback, `callback`, in a loop here.
        if ssh_username_requested {
            debug_assert!(res.is_err());
            let mut attempts = Vec::new();
            attempts.push("git".to_string());

            if let Ok(s) = env::var("USER").or_else(|_| env::var("USERNAME")) {
                attempts.push(s);
            }

            if let Some(ref s) = cred_helper.username {
                attempts.push(s.clone());
            }

            while let Some(s) = attempts.pop() {
                // We should get `USERNAME` first, where we just return our attempt,
                // and then after that we should get `SSH_KEY`. If the first attempt
                // fails we'll get called again, but we don't have another option so
                // we bail out.
                let mut attempts = 0;

                res = callback(&mut |_url, username, allowed| {
                    if allowed.contains(git2::USERNAME) {
                        return git2::Cred::username(&s);
                    }

                    if allowed.contains(git2::SSH_KEY) {
                        debug_assert_eq!(Some(&s[..]), username);

                        attempts += 1;

                        if attempts == 1 {
                            ssh_agent_attempts.push(s.to_string());
                            return git2::Cred::ssh_key_from_agent(&s);
                        }
                    }

                    Err(git2::Error::from_str("no authentication available"))
                });

                // If we made two attempts then that means:
                //
                // 1. A username was requested, we returned `s`.
                // 2. An ssh key was requested, we returned to look up `s` in the
                //    ssh agent.
                // 3. For whatever reason that lookup failed, so we were asked again
                //    for another mode of authentication.
                //
                // Essentially, if `attempts == 2` then in theory the only error was
                // that this username failed to authenticate (e.g. no other network
                // errors happened). Otherwise something else is funny so we bail
                // out.
                if attempts != 2 {
                    break;
                }
            }
        }

        if res.is_ok() || !any_attempts {
            return res.map_err(From::from);
        }

        // In the case of an authentication failure (where we tried something) then
        // we try to give a more helpful error message about precisely what we
        // tried.
        res.map_err(Error::from).chain_err(|| {
            let mut msg = "failed to authenticate when downloading repository".to_string();

            if !ssh_agent_attempts.is_empty() {
                let names = ssh_agent_attempts
                    .iter()
                    .map(|s| format!("`{}`", s))
                    .collect::<Vec<_>>()
                    .join(", ");

                msg.push_str(&format!(
                    "\nattempted ssh-agent authentication, but none of the \
                     usernames ({}) succeeded",
                    names
                ));
            }

            if let Some(failed_cred_helper) = cred_helper_bad {
                if failed_cred_helper {
                    msg.push_str(
                        "\nattempted to find username/password via git's `credential.helper` \
                         support, but failed",
                    );
                } else {
                    msg.push_str(
                        "\nattempted to find username/password via `credential.helper`, but maybe \
                         the found credentials were incorrect",
                    );
                }
            }

            msg
        })
    }

    pub fn reset(&self, revspec: &str) -> Result<()> {
        let oid = {
            let obj = self.repository.revparse_single(revspec)?;

            match obj.as_tag() {
                Some(tag) => tag.target_id(),
                None => obj.id(),
            }
        };

        let object = self.repository.find_object(oid, None)?;
        self.repository.reset(&object, git2::ResetType::Hard, None)?;
        Ok(())
    }

    pub fn update(&self) -> Result<()> {
        info!("Updating {}", self.remote);

        self.with_authentication(
            &self.repository.config()?,
            |callback| {
                let mut remote_callbacks = git2::RemoteCallbacks::new();
                remote_callbacks.credentials(callback);

                // Create a local anonymous remote in the repository to fetch the url
                let mut remote = self.repository.remote_anonymous(self.remote.as_str())?;
                let mut opts = git2::FetchOptions::new();

                opts.remote_callbacks(remote_callbacks).download_tags(
                    git2::AutotagOption::All,
                );

                remote.fetch(
                    &[self.revspec.as_str()],
                    Some(&mut opts),
                    None,
                )?;

                Ok(())
            },
        )?;

        self.reset(FETCH_HEAD)
    }
}
<|endoftext|>


<commit_before>//! The `Slot` and `Epoch` types are defined as new types over u64 to enforce type-safety between
//! the two types.
//!
//! `Slot` and `Epoch` have implementations which permit conversion, comparison and math operations
//! between each and `u64`, however specifically not between each other.
//!
//! All math operations on `Slot` and `Epoch` are saturating, they never wrap.
//!
//! It would be easy to define `PartialOrd` and other traits generically across all types which
//! implement `Into<u64>`, however this would allow operations between `Slots` and `Epochs` which
//! may lead to programming errors which are not detected by the compiler.

use crate::slot_height::SlotHeight;
use crate::test_utils::TestRandom;
use rand::RngCore;
use serde_derive::{Deserialize, Serialize};
use slog;
use ssz::{ssz_encode, Decode, DecodeError, Encode};
use std::cmp::{Ord, Ordering};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::iter::Iterator;
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Rem, Sub, SubAssign};

#[derive(Eq, Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Slot(u64);

#[derive(Eq, Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct Epoch(u64);

impl_common!(Slot);
impl_common!(Epoch);

impl Slot {
    pub fn new(slot: u64) -> Slot {
        Slot(slot)
    }

    pub fn epoch(self, slots_per_epoch: u64) -> Epoch {
        Epoch::from(self.0 / slots_per_epoch)
    }

    pub fn height(self, genesis_slot: Slot) -> SlotHeight {
        SlotHeight::from(self.0.saturating_sub(genesis_slot.as_u64()))
    }

    pub fn max_value() -> Slot {
        Slot(u64::max_value())
    }
}

impl Epoch {
    pub fn new(slot: u64) -> Epoch {
        Epoch(slot)
    }

    pub fn max_value() -> Epoch {
        Epoch(u64::max_value())
    }

    /// The first slot in the epoch.
    pub fn start_slot(self, slots_per_epoch: u64) -> Slot {
        Slot::from(self.0.saturating_mul(slots_per_epoch))
    }

    /// The last slot in the epoch.
    pub fn end_slot(self, slots_per_epoch: u64) -> Slot {
        Slot::from(
            self.0
                .saturating_add(1)
                .saturating_mul(slots_per_epoch)
                .saturating_sub(1),
        )
    }

    /// Position of some slot inside an epoch, if any.
    ///
    /// E.g., the first `slot` in `epoch` is at position `0`.
    pub fn position(self, slot: Slot, slots_per_epoch: u64) -> Option<usize> {
        let start = self.start_slot(slots_per_epoch);
        let end = self.end_slot(slots_per_epoch);

        if (slot >= start) && (slot <= end) {
            Some(slot.as_usize() - start.as_usize())
        } else {
            None
        }
    }

    pub fn slot_iter(&self, slots_per_epoch: u64) -> SlotIter {
        SlotIter {
            current_iteration: 0,
            epoch: self,
            slots_per_epoch,
        }
    }
}

pub struct SlotIter<'a> {
    current_iteration: u64,
    epoch: &'a Epoch,
    slots_per_epoch: u64,
}

impl<'a> Iterator for SlotIter<'a> {
    type Item = Slot;

    fn next(&mut self) -> Option<Slot> {
        if self.current_iteration >= self.slots_per_epoch {
            None
        } else {
            let start_slot = self.epoch.start_slot(self.slots_per_epoch);
            let previous = self.current_iteration;
            self.current_iteration += 1;
            Some(start_slot + previous)
        }
    }
}

#[cfg(test)]
mod slot_tests {
    use super::*;

    all_tests!(Slot);
}

#[cfg(test)]
mod epoch_tests {
    use super::*;

    all_tests!(Epoch);

    #[test]
    fn epoch_start_end() {
        let slots_per_epoch = 8;

        let epoch = Epoch::new(0);

        assert_eq!(epoch.start_slot(slots_per_epoch), Slot::new(0));
        assert_eq!(epoch.end_slot(slots_per_epoch), Slot::new(7));
    }

    #[test]
    fn position() {
        let slots_per_epoch = 8;

        let epoch = Epoch::new(0);
        assert_eq!(epoch.position(Slot::new(0), slots_per_epoch), Some(0));
        assert_eq!(epoch.position(Slot::new(1), slots_per_epoch), Some(1));
        assert_eq!(epoch.position(Slot::new(2), slots_per_epoch), Some(2));
        assert_eq!(epoch.position(Slot::new(3), slots_per_epoch), Some(3));
        assert_eq!(epoch.position(Slot::new(4), slots_per_epoch), Some(4));
        assert_eq!(epoch.position(Slot::new(5), slots_per_epoch), Some(5));
        assert_eq!(epoch.position(Slot::new(6), slots_per_epoch), Some(6));
        assert_eq!(epoch.position(Slot::new(7), slots_per_epoch), Some(7));
        assert_eq!(epoch.position(Slot::new(8), slots_per_epoch), None);

        let epoch = Epoch::new(1);
        assert_eq!(epoch.position(Slot::new(7), slots_per_epoch), None);
        assert_eq!(epoch.position(Slot::new(8), slots_per_epoch), Some(0));
    }

    #[test]
    fn slot_iter() {
        let slots_per_epoch = 8;

        let epoch = Epoch::new(0);

        let mut slots = vec![];
        for slot in epoch.slot_iter(slots_per_epoch) {
            slots.push(slot);
        }

        assert_eq!(slots.len(), slots_per_epoch as usize);

        for i in 0..slots_per_epoch {
            assert_eq!(Slot::from(i), slots[i as usize])
        }
    }

    #[test]
    fn max_epoch_ssz() {
        let max_epoch = Epoch::max_value();
        assert_eq!(
            &max_epoch.as_ssz_bytes(),
            &[255, 255, 255, 255, 255, 255, 255, 255]
        );
        assert_eq!(
            max_epoch,
            Epoch::from_ssz_bytes(&max_epoch.as_ssz_bytes()).unwrap()
        );
    }

    #[test]
    fn epoch_max_value_json() {
        let x: Epoch = Epoch::from(u64::max_value());
        let json = serde_json::to_string(&x).expect("should json encode");

        assert_eq!(&json, "18446744073709552000");
        assert_eq!(
            serde_json::from_str::<Epoch>(&json).expect("should json decode"),
            x
        );
    }
}
<commit_msg>Remove JSON epoch test<commit_after>//! The `Slot` and `Epoch` types are defined as new types over u64 to enforce type-safety between
//! the two types.
//!
//! `Slot` and `Epoch` have implementations which permit conversion, comparison and math operations
//! between each and `u64`, however specifically not between each other.
//!
//! All math operations on `Slot` and `Epoch` are saturating, they never wrap.
//!
//! It would be easy to define `PartialOrd` and other traits generically across all types which
//! implement `Into<u64>`, however this would allow operations between `Slots` and `Epochs` which
//! may lead to programming errors which are not detected by the compiler.

use crate::slot_height::SlotHeight;
use crate::test_utils::TestRandom;
use rand::RngCore;
use serde_derive::{Deserialize, Serialize};
use slog;
use ssz::{ssz_encode, Decode, DecodeError, Encode};
use std::cmp::{Ord, Ordering};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::iter::Iterator;
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Rem, Sub, SubAssign};

#[derive(Eq, Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Slot(u64);

#[derive(Eq, Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct Epoch(u64);

impl_common!(Slot);
impl_common!(Epoch);

impl Slot {
    pub fn new(slot: u64) -> Slot {
        Slot(slot)
    }

    pub fn epoch(self, slots_per_epoch: u64) -> Epoch {
        Epoch::from(self.0 / slots_per_epoch)
    }

    pub fn height(self, genesis_slot: Slot) -> SlotHeight {
        SlotHeight::from(self.0.saturating_sub(genesis_slot.as_u64()))
    }

    pub fn max_value() -> Slot {
        Slot(u64::max_value())
    }
}

impl Epoch {
    pub fn new(slot: u64) -> Epoch {
        Epoch(slot)
    }

    pub fn max_value() -> Epoch {
        Epoch(u64::max_value())
    }

    /// The first slot in the epoch.
    pub fn start_slot(self, slots_per_epoch: u64) -> Slot {
        Slot::from(self.0.saturating_mul(slots_per_epoch))
    }

    /// The last slot in the epoch.
    pub fn end_slot(self, slots_per_epoch: u64) -> Slot {
        Slot::from(
            self.0
                .saturating_add(1)
                .saturating_mul(slots_per_epoch)
                .saturating_sub(1),
        )
    }

    /// Position of some slot inside an epoch, if any.
    ///
    /// E.g., the first `slot` in `epoch` is at position `0`.
    pub fn position(self, slot: Slot, slots_per_epoch: u64) -> Option<usize> {
        let start = self.start_slot(slots_per_epoch);
        let end = self.end_slot(slots_per_epoch);

        if (slot >= start) && (slot <= end) {
            Some(slot.as_usize() - start.as_usize())
        } else {
            None
        }
    }

    pub fn slot_iter(&self, slots_per_epoch: u64) -> SlotIter {
        SlotIter {
            current_iteration: 0,
            epoch: self,
            slots_per_epoch,
        }
    }
}

pub struct SlotIter<'a> {
    current_iteration: u64,
    epoch: &'a Epoch,
    slots_per_epoch: u64,
}

impl<'a> Iterator for SlotIter<'a> {
    type Item = Slot;

    fn next(&mut self) -> Option<Slot> {
        if self.current_iteration >= self.slots_per_epoch {
            None
        } else {
            let start_slot = self.epoch.start_slot(self.slots_per_epoch);
            let previous = self.current_iteration;
            self.current_iteration += 1;
            Some(start_slot + previous)
        }
    }
}

#[cfg(test)]
mod slot_tests {
    use super::*;

    all_tests!(Slot);
}

#[cfg(test)]
mod epoch_tests {
    use super::*;

    all_tests!(Epoch);

    #[test]
    fn epoch_start_end() {
        let slots_per_epoch = 8;

        let epoch = Epoch::new(0);

        assert_eq!(epoch.start_slot(slots_per_epoch), Slot::new(0));
        assert_eq!(epoch.end_slot(slots_per_epoch), Slot::new(7));
    }

    #[test]
    fn position() {
        let slots_per_epoch = 8;

        let epoch = Epoch::new(0);
        assert_eq!(epoch.position(Slot::new(0), slots_per_epoch), Some(0));
        assert_eq!(epoch.position(Slot::new(1), slots_per_epoch), Some(1));
        assert_eq!(epoch.position(Slot::new(2), slots_per_epoch), Some(2));
        assert_eq!(epoch.position(Slot::new(3), slots_per_epoch), Some(3));
        assert_eq!(epoch.position(Slot::new(4), slots_per_epoch), Some(4));
        assert_eq!(epoch.position(Slot::new(5), slots_per_epoch), Some(5));
        assert_eq!(epoch.position(Slot::new(6), slots_per_epoch), Some(6));
        assert_eq!(epoch.position(Slot::new(7), slots_per_epoch), Some(7));
        assert_eq!(epoch.position(Slot::new(8), slots_per_epoch), None);

        let epoch = Epoch::new(1);
        assert_eq!(epoch.position(Slot::new(7), slots_per_epoch), None);
        assert_eq!(epoch.position(Slot::new(8), slots_per_epoch), Some(0));
    }

    #[test]
    fn slot_iter() {
        let slots_per_epoch = 8;

        let epoch = Epoch::new(0);

        let mut slots = vec![];
        for slot in epoch.slot_iter(slots_per_epoch) {
            slots.push(slot);
        }

        assert_eq!(slots.len(), slots_per_epoch as usize);

        for i in 0..slots_per_epoch {
            assert_eq!(Slot::from(i), slots[i as usize])
        }
    }

    #[test]
    fn max_epoch_ssz() {
        let max_epoch = Epoch::max_value();
        assert_eq!(
            &max_epoch.as_ssz_bytes(),
            &[255, 255, 255, 255, 255, 255, 255, 255]
        );
        assert_eq!(
            max_epoch,
            Epoch::from_ssz_bytes(&max_epoch.as_ssz_bytes()).unwrap()
        );
    }
}
<|endoftext|>


<commit_before>use std::cmp::min;
#[allow(deprecated)]
use std::mem::uninitialized;
use std::ptr::{copy_nonoverlapping, read_unaligned};

/// Generate a random frame mask.
#[inline]
pub fn generate_mask() -> [u8; 4] {
    rand::random()
}

/// Mask/unmask a frame.
#[inline]
pub fn apply_mask(buf: &mut [u8], mask: [u8; 4]) {
    apply_mask_fast32(buf, mask)
}

/// A safe unoptimized mask application.
#[inline]
#[allow(dead_code)]
fn apply_mask_fallback(buf: &mut [u8], mask: [u8; 4]) {
    for (i, byte) in buf.iter_mut().enumerate() {
        *byte ^= mask[i & 3];
    }
}

/// Faster version of `apply_mask()` which operates on 4-byte blocks.
#[inline]
#[allow(dead_code, clippy::cast_ptr_alignment)]
fn apply_mask_fast32(buf: &mut [u8], mask: [u8; 4]) {
    let mask_u32: u32 = unsafe { read_unaligned(mask.as_ptr() as *const u32) };

    let mut ptr = buf.as_mut_ptr();
    let mut len = buf.len();

    // Possible first unaligned block.
    let head = min(len, (4 - (ptr as usize & 3)) & 3);
    let mask_u32 = if head > 0 {
        unsafe {
            xor_mem(ptr, mask_u32, head);
            ptr = ptr.add(head);
        }
        len -= head;
        if cfg!(target_endian = "big") {
            mask_u32.rotate_left(8 * head as u32)
        } else {
            mask_u32.rotate_right(8 * head as u32)
        }
    } else {
        mask_u32
    };

    if len > 0 {
        debug_assert_eq!(ptr as usize % 4, 0);
    }

    // Properly aligned middle of the data.
    while len > 4 {
        unsafe {
            *(ptr as *mut u32) ^= mask_u32;
            ptr = ptr.offset(4);
            len -= 4;
        }
    }

    // Possible last block.
    if len > 0 {
        unsafe {
            xor_mem(ptr, mask_u32, len);
        }
    }
}

#[inline]
// TODO: copy_nonoverlapping here compiles to call memcpy. While it is not so inefficient,
// it could be done better. The compiler does not see that len is limited to 3.
unsafe fn xor_mem(ptr: *mut u8, mask: u32, len: usize) {
    #[allow(deprecated)]
    let mut b: u32 = uninitialized();
    #[allow(trivial_casts)]
    copy_nonoverlapping(ptr, &mut b as *mut _ as *mut u8, len);
    b ^= mask;
    #[allow(trivial_casts)]
    copy_nonoverlapping(&b as *const _ as *const u8, ptr, len);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_apply_mask() {
        let mask = [0x6d, 0xb6, 0xb2, 0x80];
        let unmasked = vec![
            0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, 0x74, 0xf9,
            0x12, 0x03,
        ];

        for data_len in 0..=unmasked.len() {
            let unmasked = &unmasked[0..data_len];
            // Check masking with different alignment.
            for off in 0..=3 {
                if unmasked.len() < off {
                    continue;
                }
                let mut masked = unmasked.to_vec();
                apply_mask_fallback(&mut masked[off..], mask);

                let mut masked_fast = unmasked.to_vec();
                apply_mask_fast32(&mut masked_fast[off..], mask);

                assert_eq!(masked, masked_fast);
            }
        }
    }
}
<commit_msg>refactoring/apply_mask: reduce number of usage of unsafe to 1<commit_after>/// Generate a random frame mask.
#[inline]
pub fn generate_mask() -> [u8; 4] {
    rand::random()
}

/// Mask/unmask a frame.
#[inline]
pub fn apply_mask(buf: &mut [u8], mask: [u8; 4]) {
    apply_mask_fast32(buf, mask)
}

/// A safe unoptimized mask application.
#[inline]
fn apply_mask_fallback(buf: &mut [u8], mask: [u8; 4]) {
    for (i, byte) in buf.iter_mut().enumerate() {
        *byte ^= mask[i & 3];
    }
}

/// Faster version of `apply_mask()` which operates on 4-byte blocks.
#[inline]
pub fn apply_mask_fast32(buf: &mut [u8], mask: [u8; 4]) {
    let mask_u32 = u32::from_ne_bytes(mask);

    let (mut prefix, words, mut suffix) = unsafe { buf.align_to_mut::<u32>() };
    apply_mask_fallback(&mut prefix, mask);
    let head = prefix.len() & 3;
    let mask_u32 = if head > 0 {
        if cfg!(target_endian = "big") {
            mask_u32.rotate_left(8 * head as u32)
        } else {
            mask_u32.rotate_right(8 * head as u32)
        }
    } else {
        mask_u32
    };
    for word in words.iter_mut() {
        *word ^= mask_u32;
    }
    apply_mask_fallback(&mut suffix, mask_u32.to_ne_bytes());
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_apply_mask() {
        let mask = [0x6d, 0xb6, 0xb2, 0x80];
        let unmasked = vec![
            0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, 0x74, 0xf9,
            0x12, 0x03,
        ];

        for data_len in 0..=unmasked.len() {
            let unmasked = &unmasked[0..data_len];
            // Check masking with different alignment.
            for off in 0..=3 {
                if unmasked.len() < off {
                    continue;
                }
                let mut masked = unmasked.to_vec();
                apply_mask_fallback(&mut masked[off..], mask);

                let mut masked_fast = unmasked.to_vec();
                apply_mask_fast32(&mut masked_fast[off..], mask);

                assert_eq!(masked, masked_fast);
            }
        }
    }
}
<|endoftext|>


<commit_before>// Copyright 2018 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Interactions with the file system.

use std::collections::HashMap;
use std::io::{self, Read, Write};
use std::fmt;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::str;
use std::time::SystemTime;

use xi_rpc::RemoteError;
use xi_rope::Rope;

use tabs::BufferId;

#[cfg(feature = "notify")]
use tabs::OPEN_FILE_EVENT_TOKEN;
#[cfg(feature = "notify")]
use watcher::FileWatcher;

const UTF8_BOM: &str = "\u{feff}";

/// Tracks all state related to open files.
pub struct FileManager {
    open_files: HashMap<PathBuf, BufferId>,
    file_info: HashMap<BufferId, FileInfo>,
    /// A monitor of filesystem events, for things like reloading changed files.
    #[cfg(feature = "notify")]
    watcher: FileWatcher,
}

#[derive(Debug)]
pub struct FileInfo {
    pub encoding: CharacterEncoding,
    pub path: PathBuf,
    pub mod_time: Option<SystemTime>,
    pub has_changed: bool,
}

pub enum FileError {
    Io(io::Error),
    UnknownEncoding,
    HasChanged,
}

#[derive(Debug, Clone, Copy)]
pub enum CharacterEncoding {
    Utf8,
    Utf8WithBom
}

impl FileManager {
    #[cfg(feature = "notify")]
    pub fn new(watcher: FileWatcher) -> Self {
        FileManager {
            open_files: HashMap::new(),
            file_info: HashMap::new(),
            watcher,
        }
    }

    #[cfg(not(feature = "notify"))]
    pub fn new() -> Self {
        FileManager {
            open_files: HashMap::new(),
            file_info: HashMap::new(),
        }
    }

    #[cfg(feature = "notify")]
    pub fn watcher(&mut self) -> &mut FileWatcher {
        &mut self.watcher
    }

    pub fn get_info(&self, id: BufferId) -> Option<&FileInfo> {
        self.file_info.get(&id)
    }

    pub fn get_editor(&self, path: &Path) -> Option<BufferId> {
        self.open_files.get(path).map(|id| *id)
    }

    /// Returns `true` if this file is open and has changed on disk.
    /// This state is stashed.
    pub fn check_file(&mut self, path: &Path, id: BufferId) -> bool {
        if let Some(info) = self.file_info.get_mut(&id) {
            let mod_t = get_mod_time(path);
            if mod_t != info.mod_time {
                info.has_changed = true
            }
            return info.has_changed;
        }
        false
    }

    pub fn open(&mut self, path: &Path, id: BufferId)
        -> Result<Rope, FileError>
    {
        if !path.exists() {
            let _ = File::create(path)?;
        }

        let (rope, info) = try_load_file(path)?;

        self.open_files.insert(path.to_owned(), id);
        if self.file_info.insert(id, info).is_none() {
            #[cfg(feature = "notify")]
            self.watcher.watch(path, false, OPEN_FILE_EVENT_TOKEN);
        }
        Ok(rope)
    }

    pub fn close(&mut self, id: BufferId) {
        if let Some(info) = self.file_info.remove(&id) {
            self.open_files.remove(&info.path);
            #[cfg(feature = "notify")]
            self.watcher.unwatch(&info.path, OPEN_FILE_EVENT_TOKEN);
        }
    }

    pub fn save(&mut self, path: &Path, text: &Rope, id: BufferId)
        -> Result<(), FileError>
    {
        let is_existing = self.file_info.contains_key(&id);
        if is_existing {
            self.save_existing(path, text, id)
        } else {
            self.save_new(path, text, id)
        }
    }

    fn save_new(&mut self, path: &Path, text: &Rope, id: BufferId)
        -> Result<(), FileError>
    {
        try_save(path, text, CharacterEncoding::Utf8)?;
        let info = FileInfo {
            encoding: CharacterEncoding::Utf8,
            path: path.to_owned(),
            mod_time: get_mod_time(path),
            has_changed: false,
        };
        self.open_files.insert(path.to_owned(), id);
        self.file_info.insert(id, info);
        #[cfg(feature = "notify")]
        self.watcher.watch(path, false, OPEN_FILE_EVENT_TOKEN);
        Ok(())
    }

    fn save_existing(&mut self, path: &Path, text: &Rope, id: BufferId)
        -> Result<(), FileError>
    {
        let prev_path = self.file_info.get(&id).unwrap().path.clone();
        if prev_path != path {
            self.save_new(path, text, id)?;
            self.open_files.remove(&prev_path);
            #[cfg(feature = "notify")]
            self.watcher.unwatch(&prev_path, OPEN_FILE_EVENT_TOKEN);
        } else if self.file_info.get(&id).unwrap().has_changed {
            return Err(FileError::HasChanged);
        } else {
            let encoding = self.file_info.get(&id).unwrap().encoding;
            try_save(path, text, encoding)?;
            self.file_info.get_mut(&id).unwrap()
                .mod_time = get_mod_time(path);
        }
        Ok(())
    }
}

fn try_load_file<P>(path: P) -> Result<(Rope, FileInfo), FileError>
where P: AsRef<Path>
{
    // TODO: support for non-utf8
    // it's arguable that the rope crate should have file loading functionality
    let mut f = File::open(path.as_ref())?;
    let mod_time = f.metadata()?.modified().ok();
    let mut bytes = Vec::new();
    f.read_to_end(&mut bytes)?;

    let encoding = CharacterEncoding::guess(&bytes);
    let rope = try_decode(bytes, encoding)?;
    let info = FileInfo {
        encoding,
        mod_time,
        path: path.as_ref().to_owned(),
        has_changed: false,
    };
    Ok((rope, info))
}

fn try_save(path: &Path, text: &Rope, encoding: CharacterEncoding)
    -> io::Result<()>
{
    let mut f = File::create(path)?;
        match encoding {
            CharacterEncoding::Utf8WithBom => f.write_all(UTF8_BOM.as_bytes())?,
            CharacterEncoding::Utf8 => (),
        }

        for chunk in text.iter_chunks(0, text.len()) {
            f.write_all(chunk.as_bytes())?;
        }
        Ok(())
}

fn try_decode(bytes: Vec<u8>,
              encoding: CharacterEncoding) -> Result<Rope, FileError> {
    match encoding {
        CharacterEncoding::Utf8 =>
            Ok(Rope::from(str::from_utf8(&bytes)?)),
        CharacterEncoding::Utf8WithBom => {
            let s = String::from_utf8(bytes).map_err(|e| e.utf8_error())?;
            Ok(Rope::from(&s[UTF8_BOM.len()..]))
        }
    }
}

impl CharacterEncoding {
    fn guess(s: &[u8]) -> Self {
        if s.starts_with(UTF8_BOM.as_bytes()) {
            CharacterEncoding::Utf8WithBom
        } else {
            CharacterEncoding::Utf8
        }
    }
}

/// Returns the modification timestamp for the file at a given path,
/// if present.
fn get_mod_time<P>(path: P) -> Option<SystemTime>
where P: AsRef<Path>
{
    File::open(path)
        .and_then(|f| f.metadata())
        .and_then(|meta| meta.modified())
        .ok()
}

impl From<io::Error> for FileError {
    fn from(src: io::Error) -> FileError {
        FileError::Io(src)
    }
}

impl From<str::Utf8Error> for FileError {
    fn from(_: str::Utf8Error) -> FileError {
        FileError::UnknownEncoding
    }
}

impl From<FileError> for RemoteError {
    fn from(_src: FileError) -> RemoteError {
        //TODO: when we migrate to using the failure crate for error handling,
        // this should return a better message
        RemoteError::custom(5, "failed to load file", None)
    }
}

impl fmt::Display for FileError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            &FileError::Io(ref e) => write!(f, "{}", e),
            &FileError::UnknownEncoding => write!(f, "Error decoding file"),
            &FileError::HasChanged => write!(f, "File has changed on disk. \
            Please save elsewhere and reload the file."),
        }
    }
}

<commit_msg>improved i/o error messages<commit_after>// Copyright 2018 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Interactions with the file system.

use std::collections::HashMap;
use std::io::{self, Read, Write};
use std::fmt;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::str;
use std::time::SystemTime;

use xi_rpc::RemoteError;
use xi_rope::Rope;

use tabs::BufferId;

#[cfg(feature = "notify")]
use tabs::OPEN_FILE_EVENT_TOKEN;
#[cfg(feature = "notify")]
use watcher::FileWatcher;

const UTF8_BOM: &str = "\u{feff}";

/// Tracks all state related to open files.
pub struct FileManager {
    open_files: HashMap<PathBuf, BufferId>,
    file_info: HashMap<BufferId, FileInfo>,
    /// A monitor of filesystem events, for things like reloading changed files.
    #[cfg(feature = "notify")]
    watcher: FileWatcher,
}

#[derive(Debug)]
pub struct FileInfo {
    pub encoding: CharacterEncoding,
    pub path: PathBuf,
    pub mod_time: Option<SystemTime>,
    pub has_changed: bool,
}

pub enum FileError {
    Io(io::Error, PathBuf),
    UnknownEncoding(PathBuf),
    HasChanged(PathBuf),
}

#[derive(Debug, Clone, Copy)]
pub enum CharacterEncoding {
    Utf8,
    Utf8WithBom
}

impl FileManager {
    #[cfg(feature = "notify")]
    pub fn new(watcher: FileWatcher) -> Self {
        FileManager {
            open_files: HashMap::new(),
            file_info: HashMap::new(),
            watcher,
        }
    }

    #[cfg(not(feature = "notify"))]
    pub fn new() -> Self {
        FileManager {
            open_files: HashMap::new(),
            file_info: HashMap::new(),
        }
    }

    #[cfg(feature = "notify")]
    pub fn watcher(&mut self) -> &mut FileWatcher {
        &mut self.watcher
    }

    pub fn get_info(&self, id: BufferId) -> Option<&FileInfo> {
        self.file_info.get(&id)
    }

    pub fn get_editor(&self, path: &Path) -> Option<BufferId> {
        self.open_files.get(path).map(|id| *id)
    }

    /// Returns `true` if this file is open and has changed on disk.
    /// This state is stashed.
    pub fn check_file(&mut self, path: &Path, id: BufferId) -> bool {
        if let Some(info) = self.file_info.get_mut(&id) {
            let mod_t = get_mod_time(path);
            if mod_t != info.mod_time {
                info.has_changed = true
            }
            return info.has_changed;
        }
        false
    }

    pub fn open(&mut self, path: &Path, id: BufferId)
        -> Result<Rope, FileError>
    {
        if !path.exists() {
            let _ = File::create(path).map_err(|e| FileError::Io(e, path.to_owned()))?;
        }

        let (rope, info) = try_load_file(path)?;

        self.open_files.insert(path.to_owned(), id);
        if self.file_info.insert(id, info).is_none() {
            #[cfg(feature = "notify")]
            self.watcher.watch(path, false, OPEN_FILE_EVENT_TOKEN);
        }
        Ok(rope)
    }

    pub fn close(&mut self, id: BufferId) {
        if let Some(info) = self.file_info.remove(&id) {
            self.open_files.remove(&info.path);
            #[cfg(feature = "notify")]
            self.watcher.unwatch(&info.path, OPEN_FILE_EVENT_TOKEN);
        }
    }

    pub fn save(&mut self, path: &Path, text: &Rope, id: BufferId)
        -> Result<(), FileError>
    {
        let is_existing = self.file_info.contains_key(&id);
        if is_existing {
            self.save_existing(path, text, id)
        } else {
            self.save_new(path, text, id)
        }
    }

    fn save_new(&mut self, path: &Path, text: &Rope, id: BufferId)
        -> Result<(), FileError>
    {
        try_save(path, text, CharacterEncoding::Utf8).map_err(|e| FileError::Io(e, path.to_owned()))?;
        let info = FileInfo {
            encoding: CharacterEncoding::Utf8,
            path: path.to_owned(),
            mod_time: get_mod_time(path),
            has_changed: false,
        };
        self.open_files.insert(path.to_owned(), id);
        self.file_info.insert(id, info);
        #[cfg(feature = "notify")]
        self.watcher.watch(path, false, OPEN_FILE_EVENT_TOKEN);
        Ok(())
    }

    fn save_existing(&mut self, path: &Path, text: &Rope, id: BufferId)
        -> Result<(), FileError>
    {
        let prev_path = self.file_info.get(&id).unwrap().path.clone();
        if prev_path != path {
            self.save_new(path, text, id)?;
            self.open_files.remove(&prev_path);
            #[cfg(feature = "notify")]
            self.watcher.unwatch(&prev_path, OPEN_FILE_EVENT_TOKEN);
        } else if self.file_info.get(&id).unwrap().has_changed {
            return Err(FileError::HasChanged(path.to_owned()));
        } else {
            let encoding = self.file_info.get(&id).unwrap().encoding;
            try_save(path, text, encoding).map_err(|e| FileError::Io(e, path.to_owned()))?;
            self.file_info.get_mut(&id).unwrap()
                .mod_time = get_mod_time(path);
        }
        Ok(())
    }
}

fn try_load_file<P>(path: P) -> Result<(Rope, FileInfo), FileError>
where P: AsRef<Path>
{
    // TODO: support for non-utf8
    // it's arguable that the rope crate should have file loading functionality
    let mut f = File::open(path.as_ref()).map_err(|e| FileError::Io(e, path.as_ref().to_owned()))?;
    let mod_time = f.metadata().map_err(|e| FileError::Io(e, path.as_ref().to_owned()))?.modified().ok();
    let mut bytes = Vec::new();
    f.read_to_end(&mut bytes).map_err(|e| FileError::Io(e, path.as_ref().to_owned()))?;

    let encoding = CharacterEncoding::guess(&bytes);
    let rope = try_decode(bytes, encoding, path.as_ref())?;
    let info = FileInfo {
        encoding,
        mod_time,
        path: path.as_ref().to_owned(),
        has_changed: false,
    };
    Ok((rope, info))
}

fn try_save(path: &Path, text: &Rope, encoding: CharacterEncoding)
    -> io::Result<()>
{
    let mut f = File::create(path)?;
        match encoding {
            CharacterEncoding::Utf8WithBom => f.write_all(UTF8_BOM.as_bytes())?,
            CharacterEncoding::Utf8 => (),
        }

        for chunk in text.iter_chunks(0, text.len()) {
            f.write_all(chunk.as_bytes())?;
        }
        Ok(())
}

fn try_decode(bytes: Vec<u8>,
              encoding: CharacterEncoding, path: &Path) -> Result<Rope, FileError> {
    match encoding {
        CharacterEncoding::Utf8 =>
            Ok(Rope::from(str::from_utf8(&bytes).map_err(|_e| FileError::UnknownEncoding(path.to_owned()))?)),
        CharacterEncoding::Utf8WithBom => {
            let s = String::from_utf8(bytes).map_err(|_e| FileError::UnknownEncoding(path.to_owned()))?;
            Ok(Rope::from(&s[UTF8_BOM.len()..]))
        }
    }
}

impl CharacterEncoding {
    fn guess(s: &[u8]) -> Self {
        if s.starts_with(UTF8_BOM.as_bytes()) {
            CharacterEncoding::Utf8WithBom
        } else {
            CharacterEncoding::Utf8
        }
    }
}

/// Returns the modification timestamp for the file at a given path,
/// if present.
fn get_mod_time<P>(path: P) -> Option<SystemTime>
where P: AsRef<Path>
{
    File::open(path)
        .and_then(|f| f.metadata())
        .and_then(|meta| meta.modified())
        .ok()
}

impl From<FileError> for RemoteError {
    fn from(src: FileError) -> RemoteError {
        //TODO: when we migrate to using the failure crate for error handling,
        // this should return a better message
		use std::error::Error;
		let mut code = 0;
		let mut message = String::new();
		match src {
			FileError::Io(e, p) => {
				code = 5;
				match p.to_str() {
					Some(s) => {
						message.push_str(e.description());
						message.push_str(" ");
						message.push_str(s);
					},
					None => {
						message.push_str(e.description());
					}
				};
			},
			FileError::UnknownEncoding(p) => {
				code = 6;
				match p.to_str() {
					Some(s) => {
						message.push_str("Error decoding file: ");
						message.push_str(s);
					},
					None => {
						message.push_str("Error decoding file");
					}
				};
			},
			FileError::HasChanged(p) => {
				code = 7;
				match p.to_str() {
					Some(s) => {
						message.push_str("File has changed on disk: ");
						message.push_str(s);
					},
					None => {
						message.push_str("File has changed on disk");
					}
				};
			}
		};
		RemoteError::custom(code, message, None)
    }
}

impl fmt::Display for FileError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            &FileError::Io(ref e, ref _p) => write!(f, "{}", e),
            &FileError::UnknownEncoding(ref _p) => write!(f, "Error decoding file"),
            &FileError::HasChanged(ref _p) => write!(f, "File has changed on disk. \
            Please save elsewhere and reload the file."),
        }
    }
}

<|endoftext|>


<commit_before>/// List of material font icons. Use this to generate a font Icon.
pub const FLOPPY_FONT_ICON: &str = "";
pub const CHECK_FONT_ICON: &str = "";
pub const ADD_FONT_ICON: &str = "";
pub const MINUS_FONT_ICON: &str = "";
<commit_msg>Add new font icons.<commit_after>/// List of material font icons. Use this to generate a font Icon.
pub const FLOPPY_FONT_ICON: &str = "";
pub const CHECK_FONT_ICON: &str = "";
pub const ADD_FONT_ICON: &str = "";
pub const MINUS_FONT_ICON: &str = "";
pub const LOAD_FONT_ICON: &str = "";
pub const SAVE_FONT_ICON: &str = "";
pub const KEYBOARD_ARROW_LEFT_FONT_ICON: &str = "";
pub const KEYBOARD_ARROW_RIGHT_FONT_ICON: &str = "";
<|endoftext|>


<commit_before>use super::super::{Binary, DirectoryStack, Shell, Variables};
use super::super::completer::*;
use liner::{BasicCompleter, CursorPosition, Event, EventKind};
use smallstring::SmallString;
use std::env;
use std::io::{self, ErrorKind, Write};
use std::mem;
use std::path::PathBuf;
use sys;
use types::*;

pub(crate) fn readln(shell: &mut Shell) -> Option<String> {
    {
        let vars_ptr = &shell.variables as *const Variables;
        let dirs_ptr = &shell.directory_stack as *const DirectoryStack;

        // Collects the current list of values from history for completion.
        let history = &shell.context.as_ref().unwrap().history.buffers.iter()
                // Map each underlying `liner::Buffer` into a `String`.
                .map(|x| x.chars().cloned().collect())
                // Collect each result into a vector to avoid borrowing issues.
                .collect::<Vec<SmallString>>();

        loop {
            let prompt = handle_prompt(shell.prompt()).unwrap();
            let funcs = &shell.functions;
            let vars = &shell.variables;
            let builtins = &shell.builtins;

            let line = shell.context.as_mut().unwrap().read_line(
                prompt,
                &mut move |Event { editor, kind }| {
                    if let EventKind::BeforeComplete = kind {
                        let (words, pos) = editor.get_words_and_cursor_position();

                        let filename = match pos {
                            CursorPosition::InWord(index) => index > 0,
                            CursorPosition::InSpace(Some(_), _) => true,
                            CursorPosition::InSpace(None, _) => false,
                            CursorPosition::OnWordLeftEdge(index) => index >= 1,
                            CursorPosition::OnWordRightEdge(index) => {
                                match (words.into_iter().nth(index), env::current_dir()) {
                                    (Some((start, end)), Ok(file)) => {
                                        let filename = editor.current_buffer().range(start, end);
                                        complete_as_file(file, filename, index)
                                    }
                                    _ => false,
                                }
                            }
                        };

                        if filename {
                            if let Ok(current_dir) = env::current_dir() {
                                if let Some(url) = current_dir.to_str() {
                                    let completer =
                                        IonFileCompleter::new(Some(url), dirs_ptr, vars_ptr);
                                    mem::replace(
                                        &mut editor.context().completer,
                                        Some(Box::new(completer)),
                                    );
                                }
                            }
                        } else {
                            // Creates a list of definitions from the shell environment that
                            // will be used
                            // in the creation of a custom completer.
                            let words = builtins.keys().iter()
                                // Add built-in commands to the completer's definitions.
                                .map(|&s| Identifier::from(s))
                                // Add the history list to the completer's definitions.
                                .chain(history.iter().cloned())
                                // Add the aliases to the completer's definitions.
                                .chain(vars.aliases.keys().cloned())
                                // Add the list of available functions to the completer's definitions.
                                .chain(funcs.keys().cloned())
                                // Add the list of available variables to the completer's definitions.
                                // TODO: We should make it free to do String->SmallString
                                //       and mostly free to go back (free if allocated)
                                .chain(vars.get_vars().map(|s| ["$", &s].concat().into()))
                                .collect();

                            // Initialize a new completer from the definitions collected.
                            let custom_completer = BasicCompleter::new(words);

                            // Creates completers containing definitions from all directories
                            // listed
                            // in the environment's **$PATH** variable.
                            let mut file_completers = if let Ok(val) = env::var("PATH") {
                                val.split(sys::PATH_SEPARATOR)
                                    .map(|s| IonFileCompleter::new(Some(s), dirs_ptr, vars_ptr))
                                    .collect()
                            } else {
                                vec![IonFileCompleter::new(Some("/bin/"), dirs_ptr, vars_ptr)]
                            };

                            // Also add files/directories in the current directory to the
                            // completion list.
                            if let Ok(current_dir) = env::current_dir() {
                                if let Some(url) = current_dir.to_str() {
                                    file_completers
                                        .push(IonFileCompleter::new(Some(url), dirs_ptr, vars_ptr));
                                }
                            }

                            // Merge the collected definitions with the file path definitions.
                            let completer = MultiCompleter::new(file_completers, custom_completer);

                            // Replace the shell's current completer with the newly-created
                            // completer.
                            mem::replace(
                                &mut editor.context().completer,
                                Some(Box::new(completer)),
                            );
                        }
                    }
                },
            );

            match line {
                Ok(line) => return Some(line),
                // Handles Ctrl + C
                Err(ref err) if err.kind() == ErrorKind::Interrupted => return None,
                // Handles Ctrl + D
                Err(ref err) if err.kind() == ErrorKind::UnexpectedEof => break,
                Err(err) => {
                    let stderr = io::stderr();
                    let mut stderr = stderr.lock();
                    let _ = writeln!(stderr, "ion: liner: {}", err);
                    return None;
                }
            }
        }
    }

    let previous_status = shell.previous_status;
    shell.exit(previous_status);
}

/// Infer if the given filename is actually a partial filename
fn complete_as_file(current_dir: PathBuf, filename: String, index: usize) -> bool {
    let filename = filename.trim();
    let mut file = current_dir.clone();
    file.push(&filename);
    // If the user explicitly requests a file through this syntax then complete as a file
    if filename.starts_with(".") {
        return true;
    }
    // If the file starts with a dollar sign, it's a variable, not a file
    if filename.starts_with("$") {
        return false;
    }
    // Once we are beyond the first string, assume its a file
    if index > 0 {
        return true;
    }
    // If we are referencing a file that exists then just complete to that file
    if file.exists() {
        return true;
    }
    // If we have a partial file inside an existing directory, e.g. /foo/b when /foo/bar
    // exists, then treat it as file as long as `foo` isn't the current directory, otherwise
    // this would apply to any string `foo`
    if let Some(parent) = file.parent() {
        return parent.exists() && parent != current_dir;
    }
    // By default assume its not a file
    false
}

/// takes the input shell,
/// prints prompt info lines, and
/// returns the last prompt line.
fn handle_prompt(full_prompt: String) -> Result<String, String> {
    if !full_prompt.contains("\n") {
        return Ok(full_prompt)
    }

    let stdout = io::stdout();
    let mut handle = stdout.lock();
    if let Err(why) =
        handle.write(full_prompt.as_bytes()) {
        return Err(format!("unable to print prompt info: {}", why));
    }

    match full_prompt.lines().last() {
        Some(prompt) => Ok(String::from(prompt)),
        None => Err(format!("unable to get prompt"))
    }
}
<commit_msg>fixed documentation<commit_after>use super::super::{Binary, DirectoryStack, Shell, Variables};
use super::super::completer::*;
use liner::{BasicCompleter, CursorPosition, Event, EventKind};
use smallstring::SmallString;
use std::env;
use std::io::{self, ErrorKind, Write};
use std::mem;
use std::path::PathBuf;
use sys;
use types::*;

pub(crate) fn readln(shell: &mut Shell) -> Option<String> {
    {
        let vars_ptr = &shell.variables as *const Variables;
        let dirs_ptr = &shell.directory_stack as *const DirectoryStack;

        // Collects the current list of values from history for completion.
        let history = &shell.context.as_ref().unwrap().history.buffers.iter()
                // Map each underlying `liner::Buffer` into a `String`.
                .map(|x| x.chars().cloned().collect())
                // Collect each result into a vector to avoid borrowing issues.
                .collect::<Vec<SmallString>>();

        loop {
            let prompt = handle_prompt(shell.prompt()).unwrap();
            let funcs = &shell.functions;
            let vars = &shell.variables;
            let builtins = &shell.builtins;

            let line = shell.context.as_mut().unwrap().read_line(
                prompt,
                &mut move |Event { editor, kind }| {
                    if let EventKind::BeforeComplete = kind {
                        let (words, pos) = editor.get_words_and_cursor_position();

                        let filename = match pos {
                            CursorPosition::InWord(index) => index > 0,
                            CursorPosition::InSpace(Some(_), _) => true,
                            CursorPosition::InSpace(None, _) => false,
                            CursorPosition::OnWordLeftEdge(index) => index >= 1,
                            CursorPosition::OnWordRightEdge(index) => {
                                match (words.into_iter().nth(index), env::current_dir()) {
                                    (Some((start, end)), Ok(file)) => {
                                        let filename = editor.current_buffer().range(start, end);
                                        complete_as_file(file, filename, index)
                                    }
                                    _ => false,
                                }
                            }
                        };

                        if filename {
                            if let Ok(current_dir) = env::current_dir() {
                                if let Some(url) = current_dir.to_str() {
                                    let completer =
                                        IonFileCompleter::new(Some(url), dirs_ptr, vars_ptr);
                                    mem::replace(
                                        &mut editor.context().completer,
                                        Some(Box::new(completer)),
                                    );
                                }
                            }
                        } else {
                            // Creates a list of definitions from the shell environment that
                            // will be used
                            // in the creation of a custom completer.
                            let words = builtins.keys().iter()
                                // Add built-in commands to the completer's definitions.
                                .map(|&s| Identifier::from(s))
                                // Add the history list to the completer's definitions.
                                .chain(history.iter().cloned())
                                // Add the aliases to the completer's definitions.
                                .chain(vars.aliases.keys().cloned())
                                // Add the list of available functions to the completer's definitions.
                                .chain(funcs.keys().cloned())
                                // Add the list of available variables to the completer's definitions.
                                // TODO: We should make it free to do String->SmallString
                                //       and mostly free to go back (free if allocated)
                                .chain(vars.get_vars().map(|s| ["$", &s].concat().into()))
                                .collect();

                            // Initialize a new completer from the definitions collected.
                            let custom_completer = BasicCompleter::new(words);

                            // Creates completers containing definitions from all directories
                            // listed
                            // in the environment's **$PATH** variable.
                            let mut file_completers = if let Ok(val) = env::var("PATH") {
                                val.split(sys::PATH_SEPARATOR)
                                    .map(|s| IonFileCompleter::new(Some(s), dirs_ptr, vars_ptr))
                                    .collect()
                            } else {
                                vec![IonFileCompleter::new(Some("/bin/"), dirs_ptr, vars_ptr)]
                            };

                            // Also add files/directories in the current directory to the
                            // completion list.
                            if let Ok(current_dir) = env::current_dir() {
                                if let Some(url) = current_dir.to_str() {
                                    file_completers
                                        .push(IonFileCompleter::new(Some(url), dirs_ptr, vars_ptr));
                                }
                            }

                            // Merge the collected definitions with the file path definitions.
                            let completer = MultiCompleter::new(file_completers, custom_completer);

                            // Replace the shell's current completer with the newly-created
                            // completer.
                            mem::replace(
                                &mut editor.context().completer,
                                Some(Box::new(completer)),
                            );
                        }
                    }
                },
            );

            match line {
                Ok(line) => return Some(line),
                // Handles Ctrl + C
                Err(ref err) if err.kind() == ErrorKind::Interrupted => return None,
                // Handles Ctrl + D
                Err(ref err) if err.kind() == ErrorKind::UnexpectedEof => break,
                Err(err) => {
                    let stderr = io::stderr();
                    let mut stderr = stderr.lock();
                    let _ = writeln!(stderr, "ion: liner: {}", err);
                    return None;
                }
            }
        }
    }

    let previous_status = shell.previous_status;
    shell.exit(previous_status);
}

/// Infer if the given filename is actually a partial filename
fn complete_as_file(current_dir: PathBuf, filename: String, index: usize) -> bool {
    let filename = filename.trim();
    let mut file = current_dir.clone();
    file.push(&filename);
    // If the user explicitly requests a file through this syntax then complete as a file
    if filename.starts_with(".") {
        return true;
    }
    // If the file starts with a dollar sign, it's a variable, not a file
    if filename.starts_with("$") {
        return false;
    }
    // Once we are beyond the first string, assume its a file
    if index > 0 {
        return true;
    }
    // If we are referencing a file that exists then just complete to that file
    if file.exists() {
        return true;
    }
    // If we have a partial file inside an existing directory, e.g. /foo/b when /foo/bar
    // exists, then treat it as file as long as `foo` isn't the current directory, otherwise
    // this would apply to any string `foo`
    if let Some(parent) = file.parent() {
        return parent.exists() && parent != current_dir;
    }
    // By default assume its not a file
    false
}

/// prints prompt info lines and
/// returns the last prompt line.
fn handle_prompt(full_prompt: String) -> Result<String, String> {
    if !full_prompt.contains("\n") {
        return Ok(full_prompt)
    }

    let stdout = io::stdout();
    let mut handle = stdout.lock();
    if let Err(why) =
        handle.write(full_prompt.as_bytes()) {
        return Err(format!("unable to print prompt info: {}", why));
    }

    match full_prompt.lines().last() {
        Some(prompt) => Ok(String::from(prompt)),
        None => Err(format!("unable to get prompt"))
    }
}
<|endoftext|>


<commit_before>use super::SystemModule;
use crate::sys::config;
use crate::sys::taskserver::Control;
use anyhow::{anyhow, ensure, Result};
use chrono::NaiveTime;
use log::info;
use serde::{Deserialize, Serialize};
use tokio::process::Command;

#[derive(Clone, Serialize, Deserialize)]
struct HealthConfig {
    enabled: bool,
    debug_exec_once: bool,
}

pub struct Health {
    config: HealthConfig,
    wakeup_list: Vec<NaiveTime>,
}

impl Health {
    pub fn new(wakeup_list: Vec<NaiveTime>) -> Result<Self> {
        info!("[health] initialize");

        let jsobj =
            config::get_object(&["health"]).map_or(Err(anyhow!("Config not found: health")), Ok)?;
        let config: HealthConfig = serde_json::from_value(jsobj)?;

        Ok(Health {
            config,
            wakeup_list,
        })
    }

    async fn health_task(&mut self, ctrl: &Control) -> Result<()> {
        let cpu_info = get_cpu_info().await;
        info!("{:?}", cpu_info);
        let mem_info = get_mem_info().await;
        info!("{:?}", mem_info?);
        let disk_info = get_disk_info().await;
        info!("{:?}", disk_info?);
        let cpu_temp = get_cpu_temp().await;
        info!("{:?}", cpu_temp?);

        Ok(())
    }

    async fn health_task_entry(ctrl: Control) -> Result<()> {
        let mut health = ctrl.sysmods().health.write().await;
        health.health_task(&ctrl).await
    }
}

impl SystemModule for Health {
    fn on_start(&self, ctrl: &Control) {
        info!("[health] on_start");
        if self.config.enabled {
            if self.config.debug_exec_once {
                ctrl.spawn_oneshot_task("health_check", Health::health_task_entry);
            } else {
                ctrl.spawn_periodic_task(
                    "health_check",
                    &self.wakeup_list,
                    Health::health_task_entry,
                );
            }
        }
    }
}

#[derive(Debug, Clone)]
struct CpuInfo {
    cpu_percent_total: f64,
    cpu_percent_list: Vec<f64>,
}

async fn get_cpu_info() -> Result<CpuInfo> {
    let buf = tokio::fs::read("/proc/stat").await?;
    let text = String::from_utf8_lossy(&buf);

    // See `man proc`
    // user   (1) Time spent in user mode.
    // nice   (2) Time spent in user mode with low priority (nice).
    // system (3) Time spent in system mode.
    // idle   (4) Time spent in the idle task.  This value should be USER_HZ times the second entry in the /proc/uptime pseudo-file.
    // iowait (since Linux 2.5.41)
    //        (5) Time waiting for I/O to complete.  This value is not reliable, for the following reasons:
    // irq (since Linux 2.6.0-test4)
    //        (6) Time servicing interrupts.
    // softirq (since Linux 2.6.0-test4)
    //        (7) Time servicing softirqs.
    // steal (since Linux 2.6.11)
    //        (8)  Stolen  time,  which  is the time spent in other operating systems when running in a virtualized environment
    // guest (since Linux 2.6.24)
    //        (9) Time spent running a virtual CPU for guest operating systems under the control of the Linux kernel.
    // guest_nice (since Linux 2.6.33)
    //        (10)  Time spent running a niced guest (virtual CPU for guest operating systems under the
    //        control of the Linux kernel).
    let mut cpu_percent_total = None;
    let mut cpu_percent_list = vec![];
    for line in text.lines() {
        let mut name = None;
        let mut user = None;
        let mut nice = None;
        let mut system = None;
        let mut idle = None;
        for (col_no, elem) in line.split_ascii_whitespace().enumerate() {
            match col_no {
                0 => name = Some(elem),
                1 => user = Some(elem),
                2 => nice = Some(elem),
                3 => system = Some(elem),
                4 => idle = Some(elem),
                _ => (),
            }
        }
        // cpu or cpu%d の行を取り出す
        if name.is_none() || !name.unwrap().starts_with("cpu") {
            continue;
        }

        let user: u64 = user.ok_or_else(|| anyhow!("parse error"))?.parse()?;
        let nice: u64 = nice.ok_or_else(|| anyhow!("parse error"))?.parse()?;
        let system: u64 = system.ok_or_else(|| anyhow!("parse error"))?.parse()?;
        let idle: u64 = idle.ok_or_else(|| anyhow!("parse error"))?.parse()?;
        let total = user + nice + system + idle;
        let value = (total - idle) as f64 / total as f64;
        if name == Some("cpu") {
            cpu_percent_total = Some(value);
        } else {
            cpu_percent_list.push(value);
        }
    }

    ensure!(cpu_percent_total.is_some());
    ensure!(!cpu_percent_list.is_empty());
    let cpu_percent_total = cpu_percent_total.unwrap();
    Ok(CpuInfo {
        cpu_percent_total,
        cpu_percent_list,
    })
}

#[derive(Debug, Clone, Copy)]
struct MemInfo {
    total_mib: f64,
    avail_mib: f64,
}

async fn get_mem_info() -> Result<MemInfo> {
    let mut cmd = Command::new("free");
    let output = cmd.output().await?;
    ensure!(output.status.success(), "free command failed");

    // sample
    //                total        used        free      shared  buff/cache   available
    // Mem:        13034888     4119272     5561960          68     3353656     8609008
    // Swap:        4194304           0     4194304
    let stdout = String::from_utf8_lossy(&output.stdout);
    let mut total = None;
    let mut avail = None;
    for (line_no, line) in stdout.lines().enumerate() {
        if line_no != 1 {
            continue;
        }
        for (col_no, elem) in line.split_ascii_whitespace().enumerate() {
            match col_no {
                1 => total = Some(elem),
                6 => avail = Some(elem),
                _ => (),
            }
        }
        break;
    }
    let total = total.ok_or_else(|| anyhow!("parse error"))?;
    let avail = avail.ok_or_else(|| anyhow!("parse error"))?;
    let total_mib = total.parse::<u64>()? as f64 / 1024.0;
    let avail_mib = avail.parse::<u64>()? as f64 / 1024.0;

    Ok(MemInfo {
        total_mib,
        avail_mib,
    })
}

#[derive(Debug, Clone, Copy)]
struct DiskInfo {
    total_gib: f64,
    avail_gib: f64,
}

async fn get_disk_info() -> Result<DiskInfo> {
    let mut cmd = Command::new("df");
    let output = cmd.output().await?;
    ensure!(output.status.success(), "df command failed");

    // sample
    // ファイルシス   1K-ブロック     使用    使用可 使用% マウント位置
    // /dev/root        122621412 12964620 104641120   12% /
    // devtmpfs           1800568        0   1800568    0% /dev
    // tmpfs              1965432        0   1965432    0% /dev/shm
    // tmpfs              1965432    17116   1948316    1% /run
    // tmpfs                 5120        4      5116    1% /run/lock
    // tmpfs              1965432        0   1965432    0% /sys/fs/cgroup
    // /dev/mmcblk0p1      258095    49324    208772   20% /boot
    // /dev/sda1         59280316 57109344         0  100% /media/usbbkup
    let stdout = String::from_utf8_lossy(&output.stdout);
    let mut total = None;
    let mut avail = None;
    for line in stdout.lines().skip(1) {
        let mut total_tmp = None;
        let mut avail_tmp = None;
        let mut mp_tmp = None;
        for (col_no, elem) in line.split_ascii_whitespace().enumerate() {
            match col_no {
                1 => total_tmp = Some(elem),
                3 => avail_tmp = Some(elem),
                5 => mp_tmp = Some(elem),
                _ => (),
            }
        }
        if let Some(mp) = mp_tmp {
            if mp == "/" {
                total = total_tmp;
                avail = avail_tmp;
            }
        }
    }
    let total = total.ok_or_else(|| anyhow!("parse error"))?;
    let avail = avail.ok_or_else(|| anyhow!("parse error"))?;
    let total_gib = total.parse::<u64>()? as f64 / 1024.0 / 1024.0;
    let avail_gib = avail.parse::<u64>()? as f64 / 1024.0 / 1024.0;

    Ok(DiskInfo {
        total_gib,
        avail_gib,
    })
}

#[derive(Debug, Clone, Copy)]
struct CpuTemp {
    temp: f64,
}

/// CPU 温度 (正確には違うかもしれない。ボード上の何らかの温度センサの値。) を取得する。
///
/// "/sys/class/thermal/thermal_zone0/temp" による。
/// デバイスファイルが存在しないと [std::io::Error] (NotFound) になる。
/// Linux 汎用のようだが少なくとも WSL2 では存在しない。
/// RasPi only で `vcgencmd measure_temp` という手もあるが、
/// 人が読みやすい代わりにパースが難しくなるのでデバイスファイルの方を使う。
async fn get_cpu_temp() -> Result<CpuTemp> {
    let buf = tokio::fs::read("/sys/class/thermal/thermal_zone0/temp").await?;
    let text = String::from_utf8_lossy(&buf);

    // 'C を 1000 倍した整数が得られるので変換する
    let temp: f64 = text.trim().parse()?;
    let temp = temp as f64 / 1000.0;

    Ok(CpuTemp { temp })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn cpu_info() {
        let info = get_cpu_info().await.unwrap();

        assert!((0.0..=100.0).contains(&info.cpu_percent_total));
        for rate in info.cpu_percent_list {
            assert!((0.0..=100.0).contains(&rate));
        }
    }

    #[tokio::test]
    async fn mem_info() {
        let info = get_mem_info().await.unwrap();

        assert!(info.avail_mib <= info.total_mib);
    }

    #[tokio::test]
    async fn disk_info() {
        let info = get_disk_info().await.unwrap();

        assert!(info.avail_gib <= info.total_gib);
    }

    #[tokio::test]
    async fn cpu_temp() {
        let result = get_cpu_temp().await;

        match result {
            Ok(info) => {
                // 取得できた場合、無難な値か確かめる
                // 変な環境でテストすると fail するので注意
                assert!(
                    (30.0..=100.0).contains(&info.temp),
                    "strange temperature: {}",
                    info.temp
                );
            }
            Err(e) => {
                let e = e.downcast::<std::io::Error>().unwrap();
                assert_eq!(e.kind(), std::io::ErrorKind::NotFound);
            }
        }
    }
}
<commit_msg>cpu_temp の仕様変更と履歴保存の準備<commit_after>use super::SystemModule;
use crate::sys::config;
use crate::sys::taskserver::Control;
use anyhow::{anyhow, ensure, Result};
use chrono::NaiveTime;
use log::info;
use serde::{Deserialize, Serialize};
use tokio::process::Command;

#[derive(Clone, Serialize, Deserialize)]
struct HealthConfig {
    enabled: bool,
    debug_exec_once: bool,
}

pub struct Health {
    config: HealthConfig,
    wakeup_list: Vec<NaiveTime>,
}

impl Health {
    pub fn new(wakeup_list: Vec<NaiveTime>) -> Result<Self> {
        info!("[health] initialize");

        let jsobj =
            config::get_object(&["health"]).map_or(Err(anyhow!("Config not found: health")), Ok)?;
        let config: HealthConfig = serde_json::from_value(jsobj)?;

        Ok(Health {
            config,
            wakeup_list,
        })
    }

    async fn health_task(&mut self, ctrl: &Control) -> Result<()> {
        let cpu_info = get_cpu_info().await;
        info!("{:?}", cpu_info);
        let mem_info = get_mem_info().await;
        info!("{:?}", mem_info?);
        let disk_info = get_disk_info().await;
        info!("{:?}", disk_info?);
        let cpu_temp = get_cpu_temp().await;
        info!("{:?}", cpu_temp?);

        Ok(())
    }

    async fn health_task_entry(ctrl: Control) -> Result<()> {
        let mut health = ctrl.sysmods().health.write().await;
        health.health_task(&ctrl).await
    }
}

impl SystemModule for Health {
    fn on_start(&self, ctrl: &Control) {
        info!("[health] on_start");
        if self.config.enabled {
            if self.config.debug_exec_once {
                ctrl.spawn_oneshot_task("health_check", Health::health_task_entry);
            } else {
                ctrl.spawn_periodic_task(
                    "health_check",
                    &self.wakeup_list,
                    Health::health_task_entry,
                );
            }
        }
    }
}

#[derive(Debug, Clone)]
struct HistoryEntry {
    cpu_info: CpuInfo,
    mem_info: MemInfo,
    disk_info: DiskInfo,
    cpu_temp: CpuTemp,
}

#[derive(Debug, Clone)]
struct CpuInfo {
    /// 全コア合計の使用率
    cpu_percent_total: f64,
    /// コアごとの使用率 (メモリ削減のためヒストリに入れる時に消す)
    cpu_percent_list: Option<Vec<f64>>,
}

#[derive(Debug, Clone, Copy)]
struct MemInfo {
    total_mib: f64,
    avail_mib: f64,
}

#[derive(Debug, Clone, Copy)]
struct DiskInfo {
    total_gib: f64,
    avail_gib: f64,
}

#[derive(Debug, Clone, Copy)]
struct CpuTemp {
    temp: Option<f64>,
}

async fn get_cpu_info() -> Result<CpuInfo> {
    let buf = tokio::fs::read("/proc/stat").await?;
    let text = String::from_utf8_lossy(&buf);

    // See `man proc`
    // user   (1) Time spent in user mode.
    // nice   (2) Time spent in user mode with low priority (nice).
    // system (3) Time spent in system mode.
    // idle   (4) Time spent in the idle task.  This value should be USER_HZ times the second entry in the /proc/uptime pseudo-file.
    // iowait (since Linux 2.5.41)
    //        (5) Time waiting for I/O to complete.  This value is not reliable, for the following reasons:
    // irq (since Linux 2.6.0-test4)
    //        (6) Time servicing interrupts.
    // softirq (since Linux 2.6.0-test4)
    //        (7) Time servicing softirqs.
    // steal (since Linux 2.6.11)
    //        (8)  Stolen  time,  which  is the time spent in other operating systems when running in a virtualized environment
    // guest (since Linux 2.6.24)
    //        (9) Time spent running a virtual CPU for guest operating systems under the control of the Linux kernel.
    // guest_nice (since Linux 2.6.33)
    //        (10)  Time spent running a niced guest (virtual CPU for guest operating systems under the
    //        control of the Linux kernel).
    let mut cpu_percent_total = None;
    let mut cpu_percent_list = vec![];
    for line in text.lines() {
        let mut name = None;
        let mut user = None;
        let mut nice = None;
        let mut system = None;
        let mut idle = None;
        for (col_no, elem) in line.split_ascii_whitespace().enumerate() {
            match col_no {
                0 => name = Some(elem),
                1 => user = Some(elem),
                2 => nice = Some(elem),
                3 => system = Some(elem),
                4 => idle = Some(elem),
                _ => (),
            }
        }
        // cpu or cpu%d の行を取り出す
        if name.is_none() || !name.unwrap().starts_with("cpu") {
            continue;
        }

        let user: u64 = user.ok_or_else(|| anyhow!("parse error"))?.parse()?;
        let nice: u64 = nice.ok_or_else(|| anyhow!("parse error"))?.parse()?;
        let system: u64 = system.ok_or_else(|| anyhow!("parse error"))?.parse()?;
        let idle: u64 = idle.ok_or_else(|| anyhow!("parse error"))?.parse()?;
        let total = user + nice + system + idle;
        let value = (total - idle) as f64 / total as f64;
        if name == Some("cpu") {
            cpu_percent_total = Some(value);
        } else {
            cpu_percent_list.push(value);
        }
    }

    ensure!(cpu_percent_total.is_some());
    ensure!(!cpu_percent_list.is_empty());
    let cpu_percent_total = cpu_percent_total.unwrap();
    let cpu_percent_list = Some(cpu_percent_list);
    Ok(CpuInfo {
        cpu_percent_total,
        cpu_percent_list,
    })
}

async fn get_mem_info() -> Result<MemInfo> {
    let mut cmd = Command::new("free");
    let output = cmd.output().await?;
    ensure!(output.status.success(), "free command failed");

    // sample
    //                total        used        free      shared  buff/cache   available
    // Mem:        13034888     4119272     5561960          68     3353656     8609008
    // Swap:        4194304           0     4194304
    let stdout = String::from_utf8_lossy(&output.stdout);
    let mut total = None;
    let mut avail = None;
    for (line_no, line) in stdout.lines().enumerate() {
        if line_no != 1 {
            continue;
        }
        for (col_no, elem) in line.split_ascii_whitespace().enumerate() {
            match col_no {
                1 => total = Some(elem),
                6 => avail = Some(elem),
                _ => (),
            }
        }
        break;
    }
    let total = total.ok_or_else(|| anyhow!("parse error"))?;
    let avail = avail.ok_or_else(|| anyhow!("parse error"))?;
    let total_mib = total.parse::<u64>()? as f64 / 1024.0;
    let avail_mib = avail.parse::<u64>()? as f64 / 1024.0;

    Ok(MemInfo {
        total_mib,
        avail_mib,
    })
}

async fn get_disk_info() -> Result<DiskInfo> {
    let mut cmd = Command::new("df");
    let output = cmd.output().await?;
    ensure!(output.status.success(), "df command failed");

    // sample
    // ファイルシス   1K-ブロック     使用    使用可 使用% マウント位置
    // /dev/root        122621412 12964620 104641120   12% /
    // devtmpfs           1800568        0   1800568    0% /dev
    // tmpfs              1965432        0   1965432    0% /dev/shm
    // tmpfs              1965432    17116   1948316    1% /run
    // tmpfs                 5120        4      5116    1% /run/lock
    // tmpfs              1965432        0   1965432    0% /sys/fs/cgroup
    // /dev/mmcblk0p1      258095    49324    208772   20% /boot
    // /dev/sda1         59280316 57109344         0  100% /media/usbbkup
    let stdout = String::from_utf8_lossy(&output.stdout);
    let mut total = None;
    let mut avail = None;
    for line in stdout.lines().skip(1) {
        let mut total_tmp = None;
        let mut avail_tmp = None;
        let mut mp_tmp = None;
        for (col_no, elem) in line.split_ascii_whitespace().enumerate() {
            match col_no {
                1 => total_tmp = Some(elem),
                3 => avail_tmp = Some(elem),
                5 => mp_tmp = Some(elem),
                _ => (),
            }
        }
        if let Some(mp) = mp_tmp {
            if mp == "/" {
                total = total_tmp;
                avail = avail_tmp;
            }
        }
    }
    let total = total.ok_or_else(|| anyhow!("parse error"))?;
    let avail = avail.ok_or_else(|| anyhow!("parse error"))?;
    let total_gib = total.parse::<u64>()? as f64 / 1024.0 / 1024.0;
    let avail_gib = avail.parse::<u64>()? as f64 / 1024.0 / 1024.0;

    Ok(DiskInfo {
        total_gib,
        avail_gib,
    })
}

/// CPU 温度 (正確には違うかもしれない。ボード上の何らかの温度センサの値。) を取得する。
///
/// "/sys/class/thermal/thermal_zone0/temp" による。
/// デバイスファイルが存在しない場合は None を返して成功扱いとする。
/// Linux 汎用のようだが少なくとも WSL2 では存在しない。
/// RasPi only で `vcgencmd measure_temp` という手もあるが、
/// 人が読みやすい代わりにパースが難しくなるのでデバイスファイルの方を使う。
async fn get_cpu_temp() -> Result<CpuTemp> {
    let result = tokio::fs::read("/sys/class/thermal/thermal_zone0/temp").await;
    match result {
        Ok(buf) => {
            let text = String::from_utf8_lossy(&buf);

            // 'C を 1000 倍した整数が得られるので変換する
            let temp = text.trim().parse::<f64>()? / 1000.0;
            let temp = Some(temp);

            Ok(CpuTemp { temp })
        }
        Err(e) => {
            if e.kind() == std::io::ErrorKind::NotFound {
                // NotFound は None を返して成功扱い
                Ok(CpuTemp { temp: None })
            } else {
                // その他のエラーはそのまま返す
                Err(anyhow::Error::from(e))
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn cpu_info() {
        let info = get_cpu_info().await.unwrap();

        assert!((0.0..=100.0).contains(&info.cpu_percent_total));
        for rate in info.cpu_percent_list.unwrap() {
            assert!((0.0..=100.0).contains(&rate));
        }
    }

    #[tokio::test]
    async fn mem_info() {
        let info = get_mem_info().await.unwrap();

        assert!(info.avail_mib <= info.total_mib);
    }

    #[tokio::test]
    async fn disk_info() {
        let info = get_disk_info().await.unwrap();

        assert!(info.avail_gib <= info.total_gib);
    }

    #[tokio::test]
    async fn cpu_temp() {
        let result = get_cpu_temp().await.unwrap();
        if let Some(temp) = result.temp {
            assert!(
                (30.0..=100.0).contains(&temp),
                "strange temperature: {}",
                temp
            );
        }
    }
}
<|endoftext|>