Compare commits

..

10 commits

Author SHA1 Message Date
Nikos Papadakis
dfd446f770
goagent: bare bones api 2024-02-17 17:20:08 +02:00
Nikos Papadakis
b4edfd7585
for go 2024-02-15 15:25:45 +02:00
Nikos Papadakis
1a5225ef6f
foo 2024-02-15 11:21:57 +02:00
Nikos Papadakis
6846e96f86
socket 2024-02-05 01:25:37 +02:00
Nikos Papadakis
6db2478786
todo: agents as genservers, messaging connection manager 2024-02-04 20:13:35 +02:00
Nikos Papadakis
3e066fd23a
subscription 2024-02-03 13:11:31 +02:00
Nikos Papadakis
32727b2ab8
add devenv + nats 2024-02-03 13:11:23 +02:00
Nikos Papadakis
59945bd2de
terminal 2024-02-02 20:53:52 +02:00
Nikos Papadakis
e0850c1d2b
make health somewhat work 2024-02-01 23:54:23 +02:00
Nikos Papadakis
b4cd5642ed
app refactoring 2024-02-01 17:34:26 +02:00
50 changed files with 1316 additions and 1048 deletions

View file

@ -6,6 +6,10 @@ end_of_line = lf
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
[*.go]
indent_style = tab
indent_size = 2
[*.nix] [*.nix]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2

2
.envrc
View file

@ -1 +1 @@
use flake use flake . --impure

2
.gitignore vendored
View file

@ -1,7 +1,7 @@
/target/ /target/
/.db/
/.vagrant/ /.vagrant/
/.direnv/ /.direnv/
/.devenv/
/deps/ /deps/
/_build/ /_build/
/priv/static/assets/ /priv/static/assets/

142
Cargo.lock generated
View file

@ -32,6 +32,12 @@ version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]]
name = "array-init"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc"
[[package]] [[package]]
name = "async-nats" name = "async-nats"
version = "0.33.0" version = "0.33.0"
@ -41,7 +47,7 @@ dependencies = [
"base64", "base64",
"bytes", "bytes",
"futures", "futures",
"http", "http 0.2.11",
"memchr", "memchr",
"nkeys", "nkeys",
"nuid", "nuid",
@ -99,6 +105,30 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "binrw"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173901312e9850391d4d7c1318c4e099fdc037d61870fca427429830efdb4e5f"
dependencies = [
"array-init",
"binrw_derive",
"bytemuck",
]
[[package]]
name = "binrw_derive"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb515fdd6f8d3a357c8e19b8ec59ef53880807864329b1cb1cba5c53bf76557e"
dependencies = [
"either",
"owo-colors",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -120,6 +150,12 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bytemuck"
version = "1.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
@ -225,7 +261,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -287,6 +323,12 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "either"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.8" version = "0.3.8"
@ -427,6 +469,23 @@ dependencies = [
"itoa", "itoa",
] ]
[[package]]
name = "http"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "httparse"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.5.0" version = "0.5.0"
@ -593,6 +652,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "owo-colors"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@ -648,7 +713,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -706,6 +771,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-nats", "async-nats",
"binrw",
"bytes", "bytes",
"chrono", "chrono",
"futures", "futures",
@ -717,6 +783,7 @@ dependencies = [
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tokio-tungstenite",
"tokio-util", "tokio-util",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
@ -962,7 +1029,7 @@ checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -993,7 +1060,18 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
] ]
[[package]] [[package]]
@ -1094,6 +1172,17 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.48" version = "2.0.48"
@ -1136,7 +1225,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -1220,7 +1309,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -1255,6 +1344,18 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-tungstenite"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite",
]
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.10" version = "0.7.10"
@ -1288,7 +1389,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -1326,6 +1427,25 @@ dependencies = [
"tracing-log", "tracing-log",
] ]
[[package]]
name = "tungstenite"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
dependencies = [
"byteorder",
"bytes",
"data-encoding",
"http 1.0.0",
"httparse",
"log",
"rand",
"sha1",
"thiserror",
"url",
"utf-8",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.17.0" version = "1.17.0"
@ -1370,6 +1490,12 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"

View file

@ -6,6 +6,7 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.79" anyhow = "1.0.79"
async-nats = "0.33.0" async-nats = "0.33.0"
binrw = "0.13.3"
bytes = "1.5.0" bytes = "1.5.0"
chrono = { version = "0.4.33", default-features = false, features = ["now", "serde"] } chrono = { version = "0.4.33", default-features = false, features = ["now", "serde"] }
futures = { version = "0.3.30", default-features = false, features = ["std"] } futures = { version = "0.3.30", default-features = false, features = ["std"] }
@ -17,6 +18,7 @@ sysinfo = { version = "0.30.5", default-features = false }
thiserror = "1.0.56" thiserror = "1.0.56"
tokio = { version = "1.35.1", features = ["full"] } tokio = { version = "1.35.1", features = ["full"] }
tokio-stream = { version = "0.1.14", default-features = false } tokio-stream = { version = "0.1.14", default-features = false }
tokio-tungstenite = "0.21.0"
tokio-util = { version = "0.7.10", features = ["codec"] } tokio-util = { version = "0.7.10", features = ["codec"] }
tracing = "0.1.40" tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["fmt"] } tracing-subscriber = { version = "0.3.18", features = ["fmt"] }

View file

@ -13,10 +13,10 @@ authorization: {
password: demo_agent_password password: demo_agent_password
permissions: { permissions: {
publish: [ publish: [
"agents.v1.demo_agent.>" "agents.v1.1.>"
] ]
subscribe: [ subscribe: [
"agents.v1.demo_agent.>" "agents.v1.1.>"
"_INBOX_demo_agent.>" "_INBOX_demo_agent.>"
] ]
} }

View file

@ -0,0 +1,83 @@
use binrw::helpers::until_eof;
use binrw::prelude::*;
#[derive(Debug, BinRead)]
enum BinaryMessage {
#[br(magic = 0u8)]
Push(PushBinaryMessage),
#[br(magic = 1u8)]
Reply(ReplyBinaryMessage),
#[br(magic = 2u8)]
Broadcast(BroadcastBinaryMessage),
}
#[derive(Debug, BinRead)]
struct PushBinaryMessage {
ref_size: u8,
join_ref_size: u8,
_topic_size: u8,
_event_size: u8,
#[br(count = ref_size)]
r#ref: Vec<u8>,
#[br(count = join_ref_size)]
join_ref: Vec<u8>,
#[br(count = _topic_size)]
topic: Vec<u8>,
#[br(count = _event_size)]
event: Vec<u8>,
#[br(parse_with = until_eof)]
payload: Vec<u8>,
}
#[derive(Debug, BinRead)]
struct ReplyBinaryMessage {
join_ref_size: u8,
ref_size: u8,
_topic_size: u8,
_status_size: u8,
#[br(count = join_ref_size)]
join_ref: Vec<u8>,
#[br(count = ref_size)]
r#ref: Vec<u8>,
#[br(count = _topic_size)]
topic: Vec<u8>,
#[br(count = _status_size)]
status: Vec<u8>,
#[br(parse_with = until_eof)]
payload: Vec<u8>,
}
#[derive(Debug, BinRead)]
struct BroadcastBinaryMessage {
_topic_size: u8,
_event_size: u8,
#[br(count = _topic_size)]
topic: Vec<u8>,
#[br(count = _event_size)]
event: Vec<u8>,
#[br(parse_with = until_eof)]
payload: Vec<u8>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_binary() {
// broadcast!("agents:1", "subject", {:binary, "hello"})
let raw_msg = vec![
2, 8, 7, 97, 103, 101, 110, 116, 115, 58, 49, 115, 117, 98, 106, 101, 99, 116, 104,
101, 108, 108, 111,
];
parse_binary_message(&raw_msg[..]).unwrap();
}
#[test]
fn parses_json() {
// broadcast!("agents:1", "subject", "hello")
let raw_msg = r#"[null, "0", "agents:1", "subject", "hello"]"#;
parse_json_message(raw_msg).unwrap();
}
}

137
agent/src/channels/mod.rs Normal file
View file

@ -0,0 +1,137 @@
mod messages;
use std::marker::PhantomData;
use futures::{SinkExt, StreamExt};
// use futures::{SinkExt, StreamExt};
use serde::{
de::{self, Visitor},
ser::SerializeTuple,
Deserialize, Serialize,
};
// use tokio_tungstenite::tungstenite::Message as WSMessage;
#[derive(Debug)]
struct Message<T> {
join_ref: Option<String>,
r#ref: String,
topic_name: String,
event_name: String,
payload: T,
}
impl<T> Serialize for Message<T>
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut msg = serializer.serialize_tuple(5)?;
msg.serialize_element(&self.join_ref)?;
msg.serialize_element(&self.r#ref)?;
msg.serialize_element(&self.topic_name)?;
msg.serialize_element(&self.event_name)?;
msg.serialize_element(&self.payload)?;
msg.end()
}
}
impl<'de, T> Deserialize<'de> for Message<T>
where
T: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct TupleVisitor<T>(PhantomData<T>);
impl<'de, T> Visitor<'de> for TupleVisitor<T>
where
T: Deserialize<'de>,
{
type Value = Message<T>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a sequence of values")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
Ok(Message {
join_ref: seq.next_element()?.ok_or_else(|| {
de::Error::custom("missing join_ref from sequence at position 0")
})?,
r#ref: seq.next_element()?.ok_or_else(|| {
de::Error::custom("missing `ref` from sequence at position 1")
})?,
topic_name: seq.next_element()?.ok_or_else(|| {
de::Error::custom("missing `topic_name` from sequence at position 2")
})?,
event_name: seq.next_element()?.ok_or_else(|| {
de::Error::custom("missing `event_name` from sequence at position 3")
})?,
payload: seq.next_element()?.ok_or_else(|| {
de::Error::custom("missing `payload` from sequence at position 4")
})?,
})
}
}
deserializer.deserialize_tuple(5, TupleVisitor(PhantomData))
}
}
pub async fn testchannel() -> anyhow::Result<()> {
let (ws_stream, _) =
tokio_tungstenite::connect_async("ws://localhost:4000/agent/websocket?vsn=2.0.0").await?;
let (mut send, mut recv) = ws_stream.split();
let msg = Message {
join_ref: Some(String::from("0")),
r#ref: String::from("0"),
topic_name: String::from("agents:1"),
event_name: String::from("phx_join"),
payload: String::from("foo"),
};
tokio::spawn(async move {
while let Some(msg) = recv.next().await {
let msg = msg.unwrap();
match msg {
tokio_tungstenite::tungstenite::Message::Binary(bytes) => {
let _ = self::messages::parse_binary_message(&bytes[..]).unwrap();
}
tokio_tungstenite::tungstenite::Message::Text(text) => {
let _ = self::messages::parse_json_message(&text).unwrap();
}
_ => todo!(),
}
}
});
send.send(tokio_tungstenite::tungstenite::Message::Text(
serde_json::to_string(&msg).unwrap(),
))
.await?;
// let msg = serde_json::to_string(&Message {
// join_ref: Some(String::from("0")),
// r#ref: "1",
// topic_name: String::from("agents:1"),
// event_name: String::from("shout"),
// payload: Some(String::from("foo")),
// })?;
// send.send(WSMessage::Text(msg)).await?;
// let msg = Message::heartbeat().to_json()?;
// send.send(WSMessage::Text(msg)).await?;
Ok(())
}

View file

@ -1,5 +1,8 @@
use tokio::signal::ctrl_c; use tokio::signal::ctrl_c;
use crate::channels::testchannel;
mod channels;
mod health; mod health;
mod messaging; mod messaging;
mod pty; mod pty;
@ -20,7 +23,8 @@ async fn main() -> anyhow::Result<()> {
} }
async fn run() -> anyhow::Result<()> { async fn run() -> anyhow::Result<()> {
let client = messaging::Client::connect("demo_agent").await?; // TODO: Configurable client
let client = messaging::Client::connect("1").await?;
init_health_subsystem(client.clone()); init_health_subsystem(client.clone());
tracing::info!("initialized health subsystem"); tracing::info!("initialized health subsystem");
@ -28,6 +32,8 @@ async fn run() -> anyhow::Result<()> {
crate::messaging::init_services(client).await?; crate::messaging::init_services(client).await?;
tracing::info!("initialized services"); tracing::info!("initialized services");
testchannel().await;
ctrl_c().await?; ctrl_c().await?;
Ok(()) Ok(())
} }

View file

@ -16,12 +16,14 @@ pub struct Client {
impl Client { impl Client {
pub async fn connect(id: &str) -> Result<Self, super::MessagingError> { pub async fn connect(id: &str) -> Result<Self, super::MessagingError> {
let prefix = format!("agents.v1.{}", id);
let nats = async_nats::ConnectOptions::with_user_and_password( let nats = async_nats::ConnectOptions::with_user_and_password(
String::from("demo_agent"), String::from("demo_agent"),
String::from("demo_agent_password"), String::from("demo_agent_password"),
) )
.name(format!("Prymn Agent {id}")) .name(format!("Prymn Agent {id}"))
.custom_inbox_prefix(format!("_INBOX_{id}")) .custom_inbox_prefix(format!("{prefix}._INBOX_"))
.connect("localhost") .connect("localhost")
.await .await
.map_err(|err| { .map_err(|err| {
@ -31,7 +33,7 @@ impl Client {
Ok(Self { Ok(Self {
id: Arc::new(String::from(id)), id: Arc::new(String::from(id)),
prefix: Arc::new(format!("agents.v1.{}", id)), prefix: Arc::new(prefix),
nats, nats,
}) })
} }

View file

@ -81,8 +81,8 @@ impl OpenTerminalMessage {
} }
} }
_close = close_stream.next() => { _close = close_stream.next() => {
tracing::info!("closing terminal");
child.kill().await.expect("the child to exit normally"); child.kill().await.expect("the child to exit normally");
tracing::info!("closing terminal");
break; break;
} }
io_result = child.wait() => { io_result = child.wait() => {

View file

@ -1,150 +1,2 @@
defmodule Prymn.Agents do defmodule Prymn.Agents do
@moduledoc ~S"""
Prymn Agents are programs that manage a remote client machine. Prymn backend
communicates with them using GRPC calls. GRPC connections are started using
the Prymn.Agents.ConnectionSupervisor and are book-kept using the
Prymn.Agents.Registry.
Agents are only valid when a `Prymn.Servers.Server` is considered registered.
"""
require Logger
alias Prymn.Agents.{Connection, Health, Agent}
alias PrymnProto.Prymn.Agent.Stub
alias PrymnProto.Prymn.{ExecRequest, SysUpdateRequest}
@doc """
Establish a connection with a Server if one does not already exist, and
return an Agent that interfaces with the rest of the system.
## Examples
iex> Prymn.Servers.get_server_by_ip!("127.0.0.1") |> Prymn.Agents.from_server()
%Prymn.Agents.Agent{}
"""
def from_server(%Prymn.Servers.Server{status: :registered} = server) do
case start_connection(server.public_ip) do
{:ok, _pid} -> Agent.new(server.public_ip)
{:error, error} -> {:error, error}
end
end
def from_server(%Prymn.Servers.Server{}) do
Logger.error("Tried to establish a connection with an unregistered server.")
{:error, :unauthorized_action}
end
@doc """
Establish a connection with a Server if one does not already exist for a
given App. Returns an [Agent] that interfaces with the rest of the system.
"""
def from_app(%Prymn.Apps.App{} = app) do
app = Prymn.Repo.preload(app, :server)
from_server(app.server)
end
@doc """
Starts a new connection with `host_address` if one does not exist.
## Examples
iex> Prymn.Agents.start_connection("127.0.0.1")
{:ok, <PID:1234>}
iex> Prymn.Agents.start_connection("127.0.0.1")
{:ok, <PID:1234>}
"""
def start_connection(host_address) do
spec = {Connection, host_address}
case DynamicSupervisor.start_child(Prymn.Agents.ConnectionSupervisor, spec) do
{:ok, pid} -> {:ok, pid}
{:error, {:already_started, pid}} -> {:ok, pid}
{:error, error} -> {:error, error}
end
end
@doc """
Subscribe to the host's Health using Phoenix.PubSub.
Broadcasted messages are the Health struct:
%Prymn.Agents.Health{}
"""
def subscribe_to_health(%Agent{} = agent) do
:ok = Health.subscribe(agent.host_address)
agent
end
def subscribe_to_health(host_address) do
:ok = Health.subscribe(host_address)
end
# TODO
# def alive?(host_address) do
# end
@doc """
Return the last known health status of the Agent, or `nil` if it doesn't
exist.
"""
def get_health(host_address) do
Health.lookup(host_address)
end
@doc """
Get the system's information (CPU, Memory usage, etc.).
"""
def get_sys_info(%Agent{} = agent) do
with {:ok, channel} <- get_channel(agent),
{:ok, result} <- Stub.get_sys_info(channel, %Google.Protobuf.Empty{}) do
result
else
{:error, error} -> {:error, error}
end
end
@doc """
Run a command.
"""
def exec(%Agent{} = agent, %ExecRequest{} = request) do
with {:ok, channel} <- get_channel(agent),
{:ok, result} <- Stub.exec(channel, request) do
result
else
{:error, error} -> {:error, error}
end
end
def exec(%Agent{} = agent, request) when is_map(request),
do: exec(agent, struct(ExecRequest, request))
@doc """
Perform a system update.
"""
def sys_update(%Agent{} = agent, %SysUpdateRequest{} = request) do
with {:ok, channel} <- get_channel(agent),
{:ok, result} <- Stub.sys_update(channel, request) do
result
else
{:error, error} -> {:error, error}
end
end
def sys_update(%Agent{} = agent, request) when is_map(request),
do: sys_update(agent, struct(SysUpdateRequest, request))
def terminal(%Agent{} = agent) do
# TODO: Find a better solve for bi-directional GRPC stream
with {:ok, channel} <- get_channel(agent),
stream <- Stub.terminal(channel) do
stream
else
{:error, error} -> {:error, error}
end
end
defp get_channel(%Agent{} = agent) do
case start_connection(agent.host_address) do
{:ok, pid} -> {:ok, Connection.get_channel(pid)}
{:error, error} -> {:error, error}
end
end
end end

View file

@ -1,13 +0,0 @@
defmodule Prymn.Agents.Agent do
@moduledoc false
defstruct [:host_address]
@type t :: %__MODULE__{
host_address: String.t()
}
def new(host_address) when is_binary(host_address) do
%__MODULE__{host_address: host_address}
end
end

View file

@ -1,140 +0,0 @@
defmodule Prymn.Agents.Connection do
@moduledoc false
alias Prymn.Agents.Health
alias PrymnProto.Prymn.Agent.Stub
require Logger
use GenServer, restart: :transient
@timeout :timer.minutes(2)
def start_link(host_address) do
GenServer.start_link(__MODULE__, host_address, name: via(host_address))
end
def get_channel(server) when is_pid(server) do
GenServer.call(server, :get_channel)
end
##
## Server callbacks
##
@impl true
def init(host) do
# Process.flag(:trap_exit, true)
pid = self()
# Start a connection without blocking the GenServer
Task.start_link(fn ->
case GRPC.Stub.connect(host, 50_012, []) do
{:ok, channel} -> send(pid, channel)
{:error, error} -> send(pid, {:connect_error, error})
end
# Keep receiving and sending back any messages to the GenServer forever
receive_loop(pid)
end)
{:ok, {host, nil}}
end
@impl true
def handle_continue(:health, {_, channel} = state) do
pid = self()
Task.start_link(fn ->
case Stub.health(channel, %Google.Protobuf.Empty{}) do
{:ok, stream} ->
# Read from the stream forever and send data back to parent
stream
|> Stream.each(fn {_, data} -> send(pid, data) end)
|> Enum.take_while(fn _ -> true end)
{:error, _rpcerror} ->
send(pid, {:connect_error, :rpc_error})
end
end)
{:noreply, state}
end
@impl true
def handle_call(:get_channel, _, {_, channel} = state) do
{:reply, channel, state, @timeout}
end
@impl true
def handle_info(%GRPC.Channel{} = channel, {host, _}) do
{:noreply, {host, channel}, {:continue, :health}}
end
def handle_info({:connect_error, reason}, {host, _} = state) do
health = Health.lookup(host, default: true)
case reason do
:timeout -> Health.make_timed_out(health)
:rpc_error -> Health.make_disconnected(health)
end
|> Health.update_and_broadcast()
# NOTE: Here we terminate normally, which means we won't be retrying. Maybe we want to?
{:stop, :normal, state}
end
def handle_info(%PrymnProto.Prymn.HealthResponse{} = response, {host, _} = state) do
response
|> Health.make_from_proto(host)
|> Health.update_and_broadcast()
{:noreply, state, @timeout}
end
def handle_info(%GRPC.RPCError{} = response, state) do
Logger.debug("received a GRPC error: #{inspect(response)}")
{:noreply, state}
end
def handle_info({:gun_up, _pid, _protocol}, state) do
# NOTE: If it's possible for the GRPC connection to be down when we receive
# this message, maybe we should restart the connection
{:noreply, state, {:continue, :health}}
end
def handle_info({:gun_down, _pid, _proto, _reason, _}, {host, _} = state) do
Health.lookup(host, default: true)
|> Health.make_disconnected()
|> Health.update_and_broadcast()
{:noreply, state, @timeout}
end
def handle_info(:timeout, state) do
{:stop, {:shutdown, :timeout}, state}
end
def handle_info(msg, state) do
Logger.debug("received unhandled message #{inspect(msg)}")
{:noreply, state}
end
@impl true
def terminate(reason, {host, channel}) do
Logger.debug("terminating Agent connection (host: #{host}, reason: #{inspect(reason)})")
Health.delete(host)
if channel, do: GRPC.Stub.disconnect(channel)
end
defp via(name) do
{:via, Registry, {Prymn.Agents.Registry, name}}
end
defp receive_loop(pid) do
receive do
msg -> send(pid, msg)
end
receive_loop(pid)
end
end

View file

@ -1,89 +0,0 @@
defmodule Prymn.Agents.Health do
@moduledoc """
The Health struct keeps simple health information of whether or not the
target host machine is up to date, has any tasks running, its resources are
getting depleted, or if it's unable be reached.
"""
defstruct [:host, :version, :status, tasks: [], message: "Unknown"]
alias PrymnProto.Prymn.HealthResponse
@type t :: %{
host: String.t(),
version: String.t(),
status: atom(),
message: String.t()
}
def start() do
:ets.new(__MODULE__, [:set, :public, :named_table, read_concurrency: true])
end
def subscribe(host) do
Phoenix.PubSub.subscribe(Prymn.PubSub, "health:#{host}")
end
def broadcast!(%__MODULE__{host: host} = health) do
Phoenix.PubSub.broadcast!(Prymn.PubSub, "health:#{host}", health)
end
def update_and_broadcast(nil) do
nil
end
def update_and_broadcast(%__MODULE__{host: host} = health) do
:ets.insert(__MODULE__, {host, health})
broadcast!(health)
end
def delete(host_address) do
:ets.delete(__MODULE__, host_address)
end
def lookup(host_address, opts \\ []) do
default = Keyword.get(opts, :default, false)
case :ets.lookup(__MODULE__, host_address) do
[{^host_address, value}] -> value
[] when default -> %__MODULE__{host: host_address}
[] -> nil
end
end
def make_timed_out(%__MODULE__{} = health) do
%__MODULE__{health | status: :unreachable, message: "Connect timed out"}
end
def make_disconnected(%__MODULE__{} = health) do
%__MODULE__{health | status: :disconnected, message: "Disconnected"}
end
def make_from_proto(%HealthResponse{system: system, version: version, tasks: tasks}, host) do
%__MODULE__{host: host, status: :connected}
|> do_version(version)
|> do_system(system)
|> do_tasks(tasks)
end
defp do_version(health, version) do
%__MODULE__{health | version: version}
end
defp do_system(health, system) do
case system.status do
"normal" -> %__MODULE__{health | message: "Connected"}
status -> %__MODULE__{health | message: "Alert: #{status}"}
end
end
defp do_tasks(health, tasks) do
tasks =
Enum.map(tasks, fn {task_key, task_value} ->
progress = Float.round(task_value.progress, 2)
{task_key, %{task_value | progress: "#{progress}%"}}
end)
%__MODULE__{health | tasks: tasks}
end
end

View file

@ -1,27 +0,0 @@
defmodule Prymn.Agents.Supervisor do
@moduledoc false
use Supervisor
@dynamic_supervisor Prymn.Agents.ConnectionSupervisor
def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
@impl true
def init(_init_arg) do
children = [
# The registry will be used to register `Connection` processes with their
# name as their address
{Registry, keys: :unique, name: Prymn.Agents.Registry},
# Dynamically start `Connection` processes
{DynamicSupervisor, name: @dynamic_supervisor, strategy: :one_for_one, max_seconds: 60}
]
# Register a "health" table that stores in-memory any agent health data
Prymn.Agents.Health.start()
Supervisor.init(children, strategy: :one_for_one)
end
end

View file

@ -14,8 +14,8 @@ defmodule Prymn.Application do
{Phoenix.PubSub, name: Prymn.PubSub}, {Phoenix.PubSub, name: Prymn.PubSub},
{Finch, name: Prymn.Finch}, {Finch, name: Prymn.Finch},
{Oban, Application.fetch_env!(:prymn, Oban)}, {Oban, Application.fetch_env!(:prymn, Oban)},
Prymn.Agents.Supervisor, Prymn.Messaging.Supervisor,
{Task.Supervisor, name: Prymn.TaskSupervisor}, # {Task.Supervisor, name: Prymn.TaskSupervisor},
PrymnWeb.Endpoint PrymnWeb.Endpoint
] ]

View file

@ -4,7 +4,6 @@ defmodule Prymn.Apps.Wordpress do
import Ecto.Changeset import Ecto.Changeset
alias Prymn.Apps.App alias Prymn.Apps.App
alias Prymn.Agents alias Prymn.Agents
alias PrymnProto.Prymn.{ExecRequest, ExecResponse}
@primary_key false @primary_key false
embedded_schema do embedded_schema do
@ -50,150 +49,150 @@ defmodule Prymn.Apps.Wordpress do
# }) # })
# |> Health.update_and_broadcast() # |> Health.update_and_broadcast()
def deploy(%App{type: "wordpress"} = app, agent, notify_fun) do def deploy(%App{type: "wordpress"} = _app, _agent, _notify_fun) do
# TODO: Run sanity checks # TODO: Run sanity checks
# e.g Database does not exist, domain does not exist, etc. # e.g Database does not exist, domain does not exist, etc.
deploy(app, agent, notify_fun, 1) # deploy(app, agent, notify_fun, 1)
end end
defp deploy(%App{wordpress: %__MODULE__{} = wp} = app, agent, notify_fun, 1 = step) do # defp deploy(%App{wordpress: %__MODULE__{} = wp} = app, agent, notify_fun, 1 = step) do
# TODO: We need a mechanism to wait for the agent to connect before proceeding, # # TODO: We need a mechanism to wait for the agent to connect before proceeding,
# this is executed faster than the connection (which happens on deploy_app in Worker) # # this is executed faster than the connection (which happens on deploy_app in Worker)
Agents.exec(agent, %ExecRequest{ # Agents.exec(agent, %ExecRequest{
user: "root", # user: "root",
program: "mysql", # program: "mysql",
args: [ # args: [
"-e", # "-e",
# TODO: Sanitize the string to protect from injection # # TODO: Sanitize the string to protect from injection
"create user '#{wp.db_user}'@'#{wp.db_host}' identified by '#{wp.db_pass}';" # "create user '#{wp.db_user}'@'#{wp.db_host}' identified by '#{wp.db_pass}';"
] # ]
}) # })
|> then(&get_results/1) # |> then(&get_results/1)
|> case do # |> case do
{_, _, exit_code} = data when exit_code != 0 -> # {_, _, exit_code} = data when exit_code != 0 ->
notify_fun.(:error, data, 1 / 5 * 100) # notify_fun.(:error, data, 1 / 5 * 100)
data -> # data ->
notify_fun.(:progress, data, step / @max_steps * 100) # notify_fun.(:progress, data, step / @max_steps * 100)
deploy(app, agent, notify_fun, step + 1) # deploy(app, agent, notify_fun, step + 1)
end # end
end # end
defp deploy(%App{wordpress: %__MODULE__{} = wp} = app, agent, notify_fun, 2 = step) do # defp deploy(%App{wordpress: %__MODULE__{} = wp} = app, agent, notify_fun, 2 = step) do
Agents.exec(agent, %ExecRequest{ # Agents.exec(agent, %ExecRequest{
user: "root", # user: "root",
program: "mysql", # program: "mysql",
args: [ # args: [
"-e", # "-e",
# TODO: Sanitize the string to protect from injection # # TODO: Sanitize the string to protect from injection
"grant all privileges on #{wp.db_name}.* to '#{wp.db_user}'@'#{wp.db_host}';" # "grant all privileges on #{wp.db_name}.* to '#{wp.db_user}'@'#{wp.db_host}';"
] # ]
}) # })
|> then(&get_results/1) # |> then(&get_results/1)
|> case do # |> case do
{_, _, exit_code} = data when exit_code != 0 -> # {_, _, exit_code} = data when exit_code != 0 ->
notify_fun.(:error, data, step / @max_steps * 100) # notify_fun.(:error, data, step / @max_steps * 100)
data -> # data ->
notify_fun.(:progress, data, step / @max_steps * 100) # notify_fun.(:progress, data, step / @max_steps * 100)
deploy(app, agent, notify_fun, step + 1) # deploy(app, agent, notify_fun, step + 1)
end # end
end # end
defp deploy(%App{wordpress: %__MODULE__{} = wp} = app, agent, notify_fun, 3 = step) do # defp deploy(%App{wordpress: %__MODULE__{} = wp} = app, agent, notify_fun, 3 = step) do
Agents.exec(agent, %ExecRequest{ # Agents.exec(agent, %ExecRequest{
user: "vagrant", # user: "vagrant",
program: "wp", # program: "wp",
args: ["core", "download", "--path=#{wp.path}"] # args: ["core", "download", "--path=#{wp.path}"]
}) # })
|> then(&get_results/1) # |> then(&get_results/1)
|> case do # |> case do
{_, _, exit_code} = data when exit_code != 0 -> # {_, _, exit_code} = data when exit_code != 0 ->
notify_fun.(:error, data, step / @max_steps * 100) # notify_fun.(:error, data, step / @max_steps * 100)
data -> # data ->
notify_fun.(:progress, data, step / @max_steps * 100) # notify_fun.(:progress, data, step / @max_steps * 100)
deploy(app, agent, notify_fun, step + 1) # deploy(app, agent, notify_fun, step + 1)
end # end
end # end
defp deploy(%App{wordpress: %__MODULE__{} = wp} = app, agent, notify_fun, 4 = step) do # defp deploy(%App{wordpress: %__MODULE__{} = wp} = app, agent, notify_fun, 4 = step) do
Agents.exec(agent, %ExecRequest{ # Agents.exec(agent, %ExecRequest{
user: "vagrant", # user: "vagrant",
program: "wp", # program: "wp",
args: [ # args: [
"config", # "config",
"create", # "create",
"--path=#{wp.path}", # "--path=#{wp.path}",
"--dbhost=#{wp.db_host}", # "--dbhost=#{wp.db_host}",
"--dbname=#{wp.db_name}", # "--dbname=#{wp.db_name}",
"--dbuser=#{wp.db_user}", # "--dbuser=#{wp.db_user}",
"--dbpass=#{wp.db_pass}" # "--dbpass=#{wp.db_pass}"
] # ]
}) # })
|> then(&get_results/1) # |> then(&get_results/1)
|> case do # |> case do
{_, _, exit_code} = data when exit_code != 0 -> # {_, _, exit_code} = data when exit_code != 0 ->
notify_fun.(:error, data, step / @max_steps * 100) # notify_fun.(:error, data, step / @max_steps * 100)
data -> # data ->
notify_fun.(:progress, data, step / @max_steps * 100) # notify_fun.(:progress, data, step / @max_steps * 100)
deploy(app, agent, notify_fun, step + 1) # deploy(app, agent, notify_fun, step + 1)
end # end
end # end
defp deploy(%App{wordpress: %__MODULE__{} = wp} = app, agent, notify_fun, 5 = step) do # defp deploy(%App{wordpress: %__MODULE__{} = wp} = app, agent, notify_fun, 5 = step) do
Agents.exec(agent, %ExecRequest{ # Agents.exec(agent, %ExecRequest{
user: "vagrant", # user: "vagrant",
program: "wp", # program: "wp",
args: ["db", "create", "--path=#{wp.path}"] # args: ["db", "create", "--path=#{wp.path}"]
}) # })
|> then(&get_results/1) # |> then(&get_results/1)
|> case do # |> case do
{_, _, exit_code} = data when exit_code != 0 -> # {_, _, exit_code} = data when exit_code != 0 ->
notify_fun.(:error, data, step / @max_steps * 100) # notify_fun.(:error, data, step / @max_steps * 100)
data -> # data ->
notify_fun.(:progress, data, step / @max_steps * 100) # notify_fun.(:progress, data, step / @max_steps * 100)
deploy(app, agent, notify_fun, step + 1) # deploy(app, agent, notify_fun, step + 1)
end # end
end # end
defp deploy(%App{name: name, wordpress: %__MODULE__{} = wp}, agent, notify_fun, 6 = step) do # defp deploy(%App{name: name, wordpress: %__MODULE__{} = wp}, agent, notify_fun, 6 = step) do
Agents.exec(agent, %ExecRequest{ # Agents.exec(agent, %ExecRequest{
user: "vagrant", # user: "vagrant",
program: "wp", # program: "wp",
args: [ # args: [
"core", # "core",
"install", # "install",
"--path=#{wp.path}", # "--path=#{wp.path}",
"--url=http://site.test", # "--url=http://site.test",
"--title=#{name}", # "--title=#{name}",
"--admin_user=#{wp.admin_username}", # "--admin_user=#{wp.admin_username}",
"--admin_email=#{wp.admin_email}" # "--admin_email=#{wp.admin_email}"
] # ]
}) # })
|> then(&get_results/1) # |> then(&get_results/1)
|> case do # |> case do
{_, _, exit_code} = data when exit_code != 0 -> # {_, _, exit_code} = data when exit_code != 0 ->
notify_fun.(:error, data, step / @max_steps * 100) # notify_fun.(:error, data, step / @max_steps * 100)
data -> # data ->
notify_fun.(:complete, data, step / @max_steps * 100) # notify_fun.(:complete, data, step / @max_steps * 100)
end # end
end # end
defp get_results(stream) do # defp get_results(stream) do
Enum.reduce_while(stream, {"", "", nil}, fn # Enum.reduce_while(stream, {"", "", nil}, fn
{:ok, %ExecResponse{out: {:exit_code, exit_code}}}, {stdout, stderr, _} -> # {:ok, %ExecResponse{out: {:exit_code, exit_code}}}, {stdout, stderr, _} ->
{:halt, {stdout, stderr, exit_code}} # {:halt, {stdout, stderr, exit_code}}
{:ok, %ExecResponse{out: {:stdout, stdout}}}, {acc_stdout, stderr, exit_code} -> # {:ok, %ExecResponse{out: {:stdout, stdout}}}, {acc_stdout, stderr, exit_code} ->
{:cont, {acc_stdout <> stdout, stderr, exit_code}} # {:cont, {acc_stdout <> stdout, stderr, exit_code}}
{:ok, %ExecResponse{out: {:stderr, stderr}}}, {stdout, acc_stderr, exit_code} -> # {:ok, %ExecResponse{out: {:stderr, stderr}}}, {stdout, acc_stderr, exit_code} ->
{:cont, {stdout, acc_stderr <> stderr, exit_code}} # {:cont, {stdout, acc_stderr <> stderr, exit_code}}
end) # end)
end # end
end end

View file

@ -0,0 +1,2 @@
defmodule Prymn.Messaging do
end

View file

@ -0,0 +1,62 @@
defmodule Prymn.Messaging.Connection do
use GenServer
require Logger
@dialyzer {:nowarn_function, init: 1}
def start_link(%{name: name} = init_arg) do
GenServer.start_link(__MODULE__, init_arg, name: name)
end
def publish(server, subject, payload) do
GenServer.call(server, {:pub, subject, payload})
end
def subscribe(server, subject, reply) do
GenServer.call(server, {:sub, subject, reply})
end
@impl true
def init(_init_arg) do
Process.flag(:trap_exit, true)
connect_opts = %{
host: "localhost",
username: "prymn_admin",
password: "prymn_admin",
auth_required: true
}
case Gnat.start_link(connect_opts) do
{:ok, pid} ->
{:ok, pid}
{:error, reason} ->
# Let the supervisor restart the Connection after a short delay
Logger.info("Initial NATS connection failed. Restarting...")
Process.sleep(1000)
{:stop, reason}
end
end
@impl true
def handle_call({:pub, subject, payload}, _from, conn_pid) do
:ok = Gnat.pub(conn_pid, subject, payload)
{:reply, :ok, conn_pid}
end
def handle_call({:sub, subject, reply}, _from, conn_pid) do
{:ok, cons_pid} = GenServer.start_link(Prymn.Messaging.FooConsumer, reply)
{:ok, sub} = Gnat.sub(conn_pid, cons_pid, subject)
{:reply, {:ok, sub}, conn_pid}
end
@impl true
def handle_info({:EXIT, pid, reason}, conn_pid) when conn_pid == pid do
Logger.info("NATS connection lost (#{reason})")
{:stop, {:shutdown, :connection_closed}, conn_pid}
end
end

View file

@ -0,0 +1,38 @@
defmodule Prymn.Messaging.ConnectionManager do
use GenServer
defstruct subscriptions: %{}
def publish(subject, payload) do
GenServer.call(__MODULE__, {:pub, subject, payload})
end
def subscribe(subject) do
GenServer.call(__MODULE__, {:pub, subject})
end
@impl true
def init(_init_arg) do
children = [
{Prymn.Messaging.Connection, %{name: :nats}}
]
Process.monitor(:nats)
{:ok, %__MODULE__{}}
end
@impl true
def handle_call({:pub, subject, payload}, _from, state) do
Prymn.Messaging.Connection.publish(:nats, subject, payload)
{:reply, :ok, state}
end
def handle_call({:sub, subject}, {pid, _}, %__MODULE__{subscriptions: subsciptions} = state) do
Prymn.Messaging.Connection.subscribe(:nats, subject, pid)
# Map.put(subsciptions)
{:reply, :ok, state}
end
end

View file

@ -0,0 +1,15 @@
defmodule Prymn.Messaging.FooConsumer do
use GenServer
@impl true
def init(reply) do
{:ok, reply}
end
@impl true
def handle_info({:msg, %{topic: subject, body: body}}, reply) do
dbg("received on fooconsumer: #{subject}")
send(reply, body)
{:noreply, reply}
end
end

View file

@ -0,0 +1,17 @@
defmodule Prymn.Messaging.Supervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(_init_arg) do
children =
[
# Prymn.Messaging.HealthConsumer
]
Supervisor.init(children, strategy: :one_for_one)
end
end

View file

@ -13,7 +13,7 @@ defmodule Prymn.Worker do
defp deploy_app(app_id) do defp deploy_app(app_id) do
pid = self() pid = self()
app = Apps.get_app!(app_id) app = Apps.get_app!(app_id)
agent = Agents.from_app(app) # agent = Agents.from_app(app)
Task.start_link(fn -> Task.start_link(fn ->
notify_fun = fn notify_fun = fn
@ -22,7 +22,7 @@ defmodule Prymn.Worker do
:error, data, _progress -> send(pid, {:error, data}) :error, data, _progress -> send(pid, {:error, data})
end end
Apps.Wordpress.deploy(app, agent, notify_fun) # Apps.Wordpress.deploy(app, agent, notify_fun)
end) end)
end end

View file

@ -0,0 +1,26 @@
defmodule PrymnWeb.AgentChannel do
use PrymnWeb, :channel
@impl true
def join("agents:" <> _agent_id, _payload, socket) do
{:ok, socket}
end
@impl true
def handle_in(event, _payload, socket) do
dbg(event)
{:noreply, socket}
end
# Channels can be used in a request/response fashion
# by sending replies to requests from the client
# @impl true
# def handle_in("ping", payload, socket) do
# {:reply, {:ok, payload}, socket}
# end
# def handle_in("binary", {:binary, data}, socket) do
# dbg(data)
# {:noreply, socket}
# end
end

View file

@ -0,0 +1,42 @@
defmodule PrymnWeb.AgentSocket do
use Phoenix.Socket
# A Socket handler
#
# It's possible to control the websocket connection and
# assign values that can be accessed by your channel topics.
channel "agents:*", PrymnWeb.AgentChannel
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
# the socket that will be set for all channels, ie
#
# {:ok, assign(socket, :user_id, verified_user_id)}
#
# To deny connection, return `:error` or `{:error, term}`. To control the
# response the client receives in that case, [define an error handler in the
# websocket
# configuration](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#socket/3-websocket-configuration).
#
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
@impl true
def connect(_params, socket, _connect_info) do
{:ok, socket}
end
# Socket id's are topics that allow you to identify all sockets for a given user:
#
# def id(socket), do: "user_socket:#{socket.assigns.user_id}"
#
# Would allow you to broadcast a "disconnect" event and terminate
# all active sockets and channels for a given user:
#
# PrymnWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
@impl true
def id(_socket), do: nil
end

View file

@ -3,17 +3,17 @@ defmodule PrymnWeb.SystemInfo do
require Logger require Logger
alias Phoenix.LiveView.AsyncResult alias Phoenix.LiveView.AsyncResult
alias PrymnProto.Prymn.SysInfoResponse
@impl true @impl true
def update(assigns, socket) do def update(assigns, socket) do
{:ok, {:ok,
socket socket
|> assign(:agent, assigns.agent) |> assign(:agent, assigns.agent)
|> assign(:sys_info, AsyncResult.loading()) |> assign(:sys_info, AsyncResult.loading())}
|> start_async(:get_sys_info, fn ->
Prymn.Agents.get_sys_info(assigns.agent) # |> start_async(:get_sys_info, fn ->
end)} # Prymn.Agents.get_sys_info(assigns.agent)
# end)}
end end
@impl true @impl true
@ -60,31 +60,32 @@ defmodule PrymnWeb.SystemInfo do
""" """
end end
def handle_async(:get_sys_info, {:ok, %SysInfoResponse{} = sys_info}, socket) do # @impl true
%{sys_info: sys_info_result, agent: agent} = socket.assigns # def handle_async(:get_sys_info, {:ok, %SysInfoResponse{} = sys_info}, socket) do
# %{sys_info: sys_info_result, agent: agent} = socket.assigns
{:noreply, # {:noreply,
socket # socket
|> assign(:sys_info, AsyncResult.ok(sys_info_result, sys_info)) # |> assign(:sys_info, AsyncResult.ok(sys_info_result, sys_info))
|> start_async(:get_sys_info, fn -> # |> start_async(:get_sys_info, fn ->
# 10 seconds is >5 which is gun's timeout duration (which might have a race # # 10 seconds is >5 which is gun's timeout duration (which might have a race
# condition if they are equal) # # condition if they are equal)
Process.sleep(:timer.seconds(10)) # Process.sleep(:timer.seconds(10))
Prymn.Agents.get_sys_info(agent) # Prymn.Agents.get_sys_info(agent)
end)} # end)}
end # end
def handle_async(:get_sys_info, {:ok, {:error, grpc_error}}, socket) do # def handle_async(:get_sys_info, {:ok, {:error, grpc_error}}, socket) do
%{sys_info: sys_info_result} = socket.assigns # %{sys_info: sys_info_result} = socket.assigns
{:noreply, # {:noreply,
socket # socket
|> assign(:sys_info, AsyncResult.failed(sys_info_result, grpc_error))} # |> assign(:sys_info, AsyncResult.failed(sys_info_result, grpc_error))}
end # end
def handle_async(:get_sys_info, {:exit, _reason}, socket) do # def handle_async(:get_sys_info, {:exit, _reason}, socket) do
{:noreply, socket} # {:noreply, socket}
end # end
defp calculate_cpu_usage(cpus) do defp calculate_cpu_usage(cpus) do
(Enum.reduce(cpus, 0, fn x, acc -> x.usage + acc end) / Enum.count(cpus)) (Enum.reduce(cpus, 0, fn x, acc -> x.usage + acc end) / Enum.count(cpus))
@ -96,13 +97,14 @@ defmodule PrymnWeb.SystemInfo do
end end
defp calculate_disk_used_percent(disks) do defp calculate_disk_used_percent(disks) do
alias PrymnProto.Prymn.SysInfoResponse.Disk 0
# alias PrymnProto.Prymn.SysInfoResponse.Disk
{used, total} = # {used, total} =
Enum.reduce(disks, {0, 0}, fn %Disk{} = disk, {used, total} -> # Enum.reduce(disks, {0, 0}, fn %Disk{} = disk, {used, total} ->
{used + disk.total_bytes - disk.avail_bytes, total + disk.total_bytes} # {used + disk.total_bytes - disk.avail_bytes, total + disk.total_bytes}
end) # end)
Float.round(100 * used / total, 2) # Float.round(100 * used / total, 2)
end end
end end

View file

@ -1,7 +1,7 @@
defmodule PrymnWeb.Terminal do defmodule PrymnWeb.Terminal do
use PrymnWeb, :live_component use PrymnWeb, :live_component
alias PrymnProto.Prymn.TerminalRequest alias Prymn.Agents
@impl true @impl true
def mount(socket) do def mount(socket) do
@ -46,75 +46,49 @@ defmodule PrymnWeb.Terminal do
end end
@impl true @impl true
def handle_event("open_terminal", _params, socket) do def handle_event("open_terminal", _params, %{assigns: assigns} = socket) do
agent = Prymn.Agents.from_server(socket.assigns.server) # TODO: make up a terminal id
pid = self() Agents.open_terminal(assigns.agent)
Task.Supervisor.start_child(Prymn.TaskSupervisor, fn -> {:noreply, assign(socket, :open, true)}
# FIXME: Have to wrap this in a Task because gun sends unsolicited messages
# to calling process
stream = Prymn.Agents.terminal(agent)
{:ok, mux_pid} =
Task.Supervisor.start_child(Prymn.TaskSupervisor, fn -> receive_loop(stream) end)
send_update(pid, PrymnWeb.Terminal, id: "terminal", mux_pid: mux_pid, open: true)
case GRPC.Stub.recv(stream, timeout: :infinity) do
{:ok, stream} ->
Enum.map(stream, fn
{:ok, %{output: data}} ->
send(mux_pid, :data)
send_update(pid, PrymnWeb.Terminal, id: "terminal", data: data)
{:error, _err} ->
send_update(pid, PrymnWeb.Terminal, id: "terminal", open: false)
end)
{:error, error} ->
dbg(error)
end
end)
{:noreply, socket}
end end
def handle_event("close_terminal", _params, socket) do def handle_event("close_terminal", _params, socket) do
send(socket.assigns.mux_pid, :close) Agents.close_terminal(socket.assigns.agent)
{:noreply, assign(socket, :open, false)} {:noreply, assign(socket, open: false, data: "")}
end end
def handle_event("data_event", data, socket) when is_binary(data) do def handle_event("data_event", data, %{assigns: assigns} = socket) when is_binary(data) do
send(socket.assigns.mux_pid, {:data_event, data}) Agents.send_terminal_input(assigns.agent, data)
{:noreply, socket} {:noreply, socket}
end end
def handle_event("resize_event", %{"cols" => cols, "rows" => rows}, socket) do def handle_event("resize_event", %{"cols" => cols, "rows" => rows}, socket) do
send(socket.assigns.mux_pid, {:resize_event, rows, cols}) Agents.resize_terminal(socket.assigns.agent, rows, cols)
{:noreply, socket} {:noreply, socket}
end end
defp receive_loop(stream) do # defp receive_loop(stream) do
receive do # receive do
{:data_event, data} -> # {:data_event, data} ->
GRPC.Stub.send_request(stream, %TerminalRequest{input: data}) # GRPC.Stub.send_request(stream, %TerminalRequest{input: data})
receive_loop(stream) # receive_loop(stream)
{:resize_event, rows, cols} -> # {:resize_event, rows, cols} ->
GRPC.Stub.send_request(stream, %TerminalRequest{ # GRPC.Stub.send_request(stream, %TerminalRequest{
resize: %TerminalRequest.Resize{rows: rows, cols: cols} # resize: %TerminalRequest.Resize{rows: rows, cols: cols}
}) # })
receive_loop(stream) # receive_loop(stream)
:data -> # :data ->
receive_loop(stream) # receive_loop(stream)
:close -> # :close ->
GRPC.Stub.send_request(stream, %TerminalRequest{input: ""}, end_stream: true) # GRPC.Stub.send_request(stream, %TerminalRequest{input: ""}, end_stream: true)
after # after
120_000 -> # 120_000 ->
GRPC.Stub.send_request(stream, %TerminalRequest{input: ""}, end_stream: true) # GRPC.Stub.send_request(stream, %TerminalRequest{input: ""}, end_stream: true)
end # end
end # end
end end

View file

@ -13,6 +13,10 @@ defmodule PrymnWeb.Endpoint do
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
socket "/agent", PrymnWeb.AgentSocket,
websocket: true,
longpoll: false
# Serve at "/" the static files from "priv/static" directory. # Serve at "/" the static files from "priv/static" directory.
# #
# You should set gzip to true if you are running phx.digest # You should set gzip to true if you are running phx.digest

View file

@ -8,23 +8,12 @@ defmodule PrymnWeb.ServerLive.Index do
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
servers = Servers.list_servers() servers = Servers.list_servers()
agents = Agents.from_servers(servers)
healths =
if connected?(socket) do
for %Servers.Server{status: :registered, public_ip: ip} = server <- servers, into: %{} do
Agents.from_server(server)
|> Agents.subscribe_to_health()
{ip, Agents.get_health(ip)}
end
else
%{}
end
{:ok, {:ok,
socket socket
|> assign(:servers, servers) |> assign(:servers, servers)
|> assign(:healths, healths)} |> assign(:agents, agents)}
end end
@impl true @impl true
@ -40,27 +29,14 @@ defmodule PrymnWeb.ServerLive.Index do
<Button.primary patch={~p"/servers/new"}>Connect a Server</Button.primary> <Button.primary patch={~p"/servers/new"}>Connect a Server</Button.primary>
</:actions> </:actions>
</.header> </.header>
<div class="space-y-5" phx-update="replace" id="servers"> <div class="mt-10 space-y-5" phx-update="replace" id="servers">
<.link <.link :for={server <- @servers} navigate={~p"/servers/#{server}"} class="group flex">
:for={server <- @servers} <.status_bar agent={@agents[server.id]} />
navigate={~p"/servers/#{server}"} <div class="flex-1 rounded-r-lg bg-gray-100 p-5 transition-colors group-hover:bg-black group-hover:text-white">
class="group block rounded-lg bg-gray-100 p-5 shadow-sm shadow-gray-300 hover:bg-black hover:text-white"
>
<div class="flex flex-row flex-wrap justify-between">
<h2 class="text-xl"><%= server.name %></h2> <h2 class="text-xl"><%= server.name %></h2>
<.server_status status={server.status} health={@healths[server.public_ip]} /> <div class="flex flex-row flex-wrap justify-between lg:text-sm">
</div> <span>IP: <%= server.public_ip || "N/A" %></span>
<div class="flex flex-row flex-wrap justify-between lg:text-sm"> </div>
<span>IP: <%= server.public_ip || "N/A" %></span>
<%= if @healths[server.public_ip] do %>
<span
:for={{name, task} <- @healths[server.public_ip].tasks || []}
class="text-right text-xs text-slate-700"
>
<div>In progress: <%= name %></div>
<div><%= task.progress %></div>
</span>
<% end %>
</div> </div>
</.link> </.link>
</div> </div>
@ -89,44 +65,26 @@ defmodule PrymnWeb.ServerLive.Index do
|> update(:servers, fn servers -> [server | servers] end)} |> update(:servers, fn servers -> [server | servers] end)}
end end
def handle_info(%Agents.Health{} = health, socket) do # def handle_info(%Agents.Agent{} = agent, socket) do
healths = Map.put(socket.assigns.healths, health.host, health) # id = String.to_integer(agent.id)
{:noreply, assign(socket, :healths, healths)} # {:noreply, update(socket, :agents, &Map.put(&1, id, agent))}
end # end
def handle_info(msg, state) do def handle_info(msg, state) do
Logger.debug("received unexpected message #{inspect(msg)}") Logger.debug("received unexpected message #{inspect(msg)}")
{:noreply, state} {:noreply, state}
end end
defp server_status(assigns) do defp status_bar(assigns) do
case {assigns.status, assigns.health} do assigns =
{:unregistered, _} -> assign(assigns, :class, [
~H""" "w-3 rounded-l-lg",
<span class="self-center text-sm text-gray-500">Needs registration</span> assigns.agent.status == :connected && "bg-teal-500",
""" assigns.agent.status == :disconnected && "bg-red-500"
])
{:registered, nil} -> ~H"""
~H""" <div class={@class}></div>
<.spinner size="md" /> """
"""
{:registered, %Agents.Health{status: :connected}} ->
~H"""
<span class="self-center text-sm text-green-600">Connected</span>
"""
{:registered, %Agents.Health{status: :disconnected}} ->
~H"""
<span class="self-center text-sm text-red-600">Disconnected</span>
"""
{:registered, %Agents.Health{message: message}} ->
assigns = assign(assigns, :message, message)
~H"""
<span class="self-center text-sm text-yellow-900"><%= @message %></span>
"""
end
end end
end end

View file

@ -2,7 +2,7 @@ defmodule PrymnWeb.ServerLive.Show do
use PrymnWeb, :live_view use PrymnWeb, :live_view
require Logger require Logger
alias Prymn.{Agents, Servers} alias Prymn.{Agents, Servers, Messaging}
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
@ -20,7 +20,7 @@ defmodule PrymnWeb.ServerLive.Show do
<.dropdown title="Select a different server"> <.dropdown title="Select a different server">
<:button variant="tertiary">Server <%= @server.name %></:button> <:button variant="tertiary">Server <%= @server.name %></:button>
<:item <:item
:for={server <- Enum.filter(@servers, fn s -> s.id != @server.id end)} :for={server <- Enum.filter(@servers, &(&1.id != @server.id))}
patch={~p"/servers/#{server}"} patch={~p"/servers/#{server}"}
> >
<%= server.name %> <%= server.name %>
@ -29,7 +29,7 @@ defmodule PrymnWeb.ServerLive.Show do
<Button.tertiary title="Edit server name" phx-click={show_edit_server_name()}> <Button.tertiary title="Edit server name" phx-click={show_edit_server_name()}>
<.icon class="h-4 w-4" name="hero-pencil-solid" /> <.icon class="h-4 w-4" name="hero-pencil-solid" />
</Button.tertiary> </Button.tertiary>
<.indicator message={@health.message} /> <.indicator message="test" />
</div> </div>
<form class="hidden items-center" id="server-name-edit" phx-submit={submit_edit_server_name()}> <form class="hidden items-center" id="server-name-edit" phx-submit={submit_edit_server_name()}>
<input class="outline-none" type="text" name="name" value={@server.name} required /> <input class="outline-none" type="text" name="name" value={@server.name} required />
@ -48,10 +48,6 @@ defmodule PrymnWeb.ServerLive.Show do
</div> </div>
</div> </div>
<span class="text-sm opacity-75"><%= @server.public_ip %></span> <span class="text-sm opacity-75"><%= @server.public_ip %></span>
<div :for={{name, task} <- @health.tasks} class="my-3 text-sm text-slate-700">
<p>Background task in progress: <%= name %></p>
<p><%= task.progress %> complete</p>
</div>
<div :if={@server.status == :unregistered} class="my-10"> <div :if={@server.status == :unregistered} class="my-10">
<p class="mb-9"> <p class="mb-9">
Connect to your server using root credentials and execute the following command: Connect to your server using root credentials and execute the following command:
@ -96,7 +92,7 @@ defmodule PrymnWeb.ServerLive.Show do
<h2 class="my-5 text-xl"> <h2 class="my-5 text-xl">
Terminal Terminal
</h2> </h2>
<.live_component id="terminal" module={PrymnWeb.Terminal} server={@server} /> <.live_component id="terminal" module={PrymnWeb.Terminal} agent={@agent} />
</section> </section>
</div> </div>
<.back navigate={~p"/servers"}>Back to servers</.back> <.back navigate={~p"/servers"}>Back to servers</.back>
@ -107,65 +103,65 @@ defmodule PrymnWeb.ServerLive.Show do
@impl true @impl true
def handle_params(%{"id" => id}, _, socket) do def handle_params(%{"id" => id}, _, socket) do
server = Servers.get_server!(id) server = Servers.get_server!(id)
agent = Agents.from_server(server)
socket =
if connected?(socket) and server.status == :registered do
agent = Agents.from_server(server)
Agents.subscribe_to_health(agent)
assign(socket, :agent, agent)
else
socket
end
health = Agents.get_health(server.public_ip)
{:noreply, {:noreply,
socket socket
|> assign(:page_title, server.name) |> assign(:page_title, server.name)
|> assign(:health, health || %{message: "Connecting...", tasks: []})
|> assign(:server, server) |> assign(:server, server)
|> assign(:agent, agent)
|> assign(:dry_run, false) |> assign(:dry_run, false)
|> assign(:update_output, []) |> assign(:update_output, [])
# TODO: Do not assign this to the socket - instead generate it in the HTML # TODO: Do not assign this to the socket - instead generate it in the HTML
|> assign(:registration_command, Servers.create_setup_command(server))} |> assign(:registration_command, Servers.create_setup_command(server))}
end end
@impl true # @impl true
def handle_info(%PrymnProto.Prymn.SysUpdateResponse{} = response, socket) do # def handle_info(%Agents.Agent{} = agent, socket) do
output = String.split(response.output, "\n") # {:noreply, assign(socket, :agent, agent)}
socket = assign(socket, :update_output, output) # end
{:noreply, socket}
end
def handle_info(%Agents.Health{host: host} = health, socket) do # def handle_info(%Messaging.Messages.TerminalOutput{output: output}, socket) do
socket = # send_update(PrymnWeb.Terminal, id: "terminal", data: output)
if host == socket.assigns.server.public_ip, # {:noreply, socket}
do: assign(socket, :health, health), # end
else: socket
{:noreply, socket} # @impl true
end # def handle_info(%PrymnProto.Prymn.SysUpdateResponse{} = response, socket) do
# output = String.split(response.output, "\n")
# socket = assign(socket, :update_output, output)
# {:noreply, socket}
# end
# def handle_info(%Agents.Health{host: host} = health, socket) do
# socket =
# if host == socket.assigns.server.public_ip,
# do: assign(socket, :health, health),
# else: socket
# {:noreply, socket}
# end
@impl true @impl true
def handle_event("system_update", _params, socket) do def handle_event("system_update", _params, socket) do
server_name = get_in(socket.assigns, [:server, Access.key(:name)]) # server_name = get_in(socket.assigns, [:server, Access.key(:name)])
pid = self() # pid = self()
if agent = socket.assigns[:agent] do # if agent = socket.assigns[:agent] do
# TODO: This is ugly # # TODO: This is ugly
Task.start_link(fn -> # Task.start_link(fn ->
Agents.sys_update(agent, %{dry_run: socket.assigns.dry_run}) # Agents.sys_update(agent, %{dry_run: socket.assigns.dry_run})
|> Stream.each(fn # |> Stream.each(fn
{:ok, msg} -> send(pid, msg) # {:ok, msg} -> send(pid, msg)
{:error, error} -> Logger.error("error during system update call: #{inspect(error)}") # {:error, error} -> Logger.error("error during system update call: #{inspect(error)}")
end) # end)
|> Enum.to_list() # |> Enum.to_list()
end) # end)
put_flash(socket, :info, "Started a system update on server #{server_name}.") # put_flash(socket, :info, "Started a system update on server #{server_name}.")
else # else
put_flash(socket, :error, "Could not perform the update.") # put_flash(socket, :error, "Could not perform the update.")
end # end
{:noreply, socket} {:noreply, socket}
end end

View file

@ -20,10 +20,65 @@
"type": "github" "type": "github"
} }
}, },
"devenv": {
"inputs": {
"flake-compat": "flake-compat",
"nix": "nix",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks"
},
"locked": {
"lastModified": 1706918346,
"narHash": "sha256-UYbmL0db7+yQNpQ3nyW5077kmtB3fT/M0h/LhosODm4=",
"owner": "cachix",
"repo": "devenv",
"rev": "e4019e17e9818cc3f107cef515d23d255e43e29a",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"inputs": { "inputs": {
"systems": "systems" "systems": "systems"
}, },
"locked": {
"lastModified": 1685518550,
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": { "locked": {
"lastModified": 1701680307, "lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
@ -38,7 +93,117 @@
"type": "github" "type": "github"
} }
}, },
"gitignore": {
"inputs": {
"nixpkgs": [
"devenv",
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1660459072,
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"lowdown-src": {
"flake": false,
"locked": {
"lastModified": 1633514407,
"narHash": "sha256-Dw32tiMjdK9t3ETl5fzGrutQTzh2rufgZV4A/BbxuD4=",
"owner": "kristapsdz",
"repo": "lowdown",
"rev": "d2c2b44ff6c27b936ec27358a2653caaef8f73b8",
"type": "github"
},
"original": {
"owner": "kristapsdz",
"repo": "lowdown",
"type": "github"
}
},
"nix": {
"inputs": {
"lowdown-src": "lowdown-src",
"nixpkgs": [
"devenv",
"nixpkgs"
],
"nixpkgs-regression": "nixpkgs-regression"
},
"locked": {
"lastModified": 1676545802,
"narHash": "sha256-EK4rZ+Hd5hsvXnzSzk2ikhStJnD63odF7SzsQ8CuSPU=",
"owner": "domenkozar",
"repo": "nix",
"rev": "7c91803598ffbcfe4a55c44ac6d49b2cf07a527f",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "relaxed-flakes",
"repo": "nix",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": {
"lastModified": 1678875422,
"narHash": "sha256-T3o6NcQPwXjxJMn2shz86Chch4ljXgZn746c2caGxd8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "126f49a01de5b7e35a43fd43f891ecf6d3a51459",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-regression": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1685801374,
"narHash": "sha256-otaSUoFEMM+LjBI1XL/xGB5ao6IwnZOXc47qhIgJe8U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c37ca420157f4abc31e26f436c1145f8951ff373",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1703134684, "lastModified": 1703134684,
"narHash": "sha256-SQmng1EnBFLzS7WSRyPM9HgmZP2kLJcPAz+Ug/nug6o=", "narHash": "sha256-SQmng1EnBFLzS7WSRyPM9HgmZP2kLJcPAz+Ug/nug6o=",
@ -54,11 +219,40 @@
"type": "github" "type": "github"
} }
}, },
"pre-commit-hooks": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"flake-utils": "flake-utils",
"gitignore": "gitignore",
"nixpkgs": [
"devenv",
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1704725188,
"narHash": "sha256-qq8NbkhRZF1vVYQFt1s8Mbgo8knj+83+QlL5LBnYGpI=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "ea96f0c05924341c551a797aaba8126334c505d2",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"crane": "crane", "crane": "crane",
"flake-utils": "flake-utils", "devenv": "devenv",
"nixpkgs": "nixpkgs", "flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay" "rust-overlay": "rust-overlay"
} }
}, },
@ -99,6 +293,21 @@
"repo": "default", "repo": "default",
"type": "github" "type": "github"
} }
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View file

@ -1,8 +1,14 @@
{ {
description = "Prymn"; description = "Prymn";
nixConfig = {
extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
extra-substituters = "https://devenv.cachix.org";
};
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
devenv.url = "github:cachix/devenv";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
rust-overlay = { rust-overlay = {
url = "github:oxalica/rust-overlay"; url = "github:oxalica/rust-overlay";
@ -12,33 +18,45 @@
crane = { crane = {
url = "github:ipetkov/crane"; url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-utils.follows = "flake-utils";
inputs.rust-overlay.follows = "rust-overlay";
}; };
}; };
outputs = { self, nixpkgs, flake-utils, rust-overlay, crane }: outputs = { self, nixpkgs, devenv, flake-utils, rust-overlay, crane }@inputs:
flake-utils.lib.eachDefaultSystem (system: flake-utils.lib.eachDefaultSystem (system:
let let
overlays = [ (import ./nix/overlay.nix) (import rust-overlay) ]; overlays = [ (import ./nix/overlay.nix) (import rust-overlay) ];
pkgs = import nixpkgs { inherit system overlays; }; pkgs = import nixpkgs { inherit system overlays; };
scripts = pkgs.callPackage ./nix/scripts.nix { };
rustBuilder = import ./nix/rust.nix { inherit crane pkgs system; }; rustBuilder = import ./nix/rust.nix { inherit crane pkgs system; };
in in
{ {
devShells.default = with pkgs; mkShell { packages = {
packages = [ devenv-up = self.devShells.${system}.default.config.procfileScript;
elixir };
elixir-ls
rustToolchain
protobuf
protoc-gen-elixir
scripts.prymn_db
] ++ lib.optionals stdenv.isLinux [ inotify-tools ];
shellHook = '' devShells.default = devenv.lib.mkShell {
export PROJECT_ROOT_DIR="$PWD" inherit pkgs inputs;
'';
modules = [
({ pkgs, config, ... }: {
packages = [
pkgs.elixir
pkgs.elixir-ls
pkgs.rustToolchain
pkgs.go
] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.inotify-tools ];
services.postgres = {
enable = true;
package = pkgs.postgresql_16;
initialDatabases = [{ name = "prymn_dev"; }];
initialScript = "CREATE USER postgres SUPERUSER;";
};
enterShell = ''
echo happy deving
'';
})
];
}; };
checks.rustTest = rustBuilder.test; checks.rustTest = rustBuilder.test;

35
goagent/cmd/agent/main.go Normal file
View file

@ -0,0 +1,35 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"nikos.codes/prymn/prymn/goagent/pkg/gophx"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
socket, err := gophx.Connect(ctx)
if err != nil {
fmt.Println(err)
}
channel := gophx.NewChannel("agents:1", nil, socket)
channel.On("foo_bar", func(payload any) {
fmt.Println("received msg: ", payload)
})
handleSignals(cancel)
}
func handleSignals(cancel context.CancelFunc) {
signalCh := make(chan os.Signal, 1)
signal.Notify(signalCh)
<-signalCh
cancel()
}

5
goagent/go.mod Normal file
View file

@ -0,0 +1,5 @@
module nikos.codes/prymn/prymn/goagent
go 1.21.5
require nhooyr.io/websocket v1.8.10 // indirect

2
goagent/go.sum Normal file
View file

@ -0,0 +1,2 @@
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=

View file

@ -0,0 +1,45 @@
package gophx
type Channel struct {
topic string
bindings map[string]func(any)
}
func NewChannel(topic string, params any, socket *Socket) *Channel {
// TODO: rejoin when socket is reconnected
channel := &Channel{
bindings: make(map[string]func(any)),
topic: topic,
}
msg := message{
joinRef: 0,
ref: 0,
topic: topic,
event: "phx_join",
payload: params,
}
data, err := msg.serializeJSON()
if err != nil {
panic(err)
}
socket.addChannel(channel)
socket.send(data)
return channel
}
func (channel *Channel) On(event string, callback func(any)) {
channel.bindings[event] = callback
}
func (channel *Channel) Push(event string, params any) {
}
func (channel *Channel) handleMessage(msg message) {
if binding, ok := channel.bindings[msg.event]; ok {
go binding(msg.payload)
}
}

View file

@ -0,0 +1,36 @@
package gophx
import (
"encoding/json"
)
type message struct {
joinRef int64
ref int64
topic string
event string
payload any
}
func (m message) serializeJSON() ([]byte, error) {
data, err := json.Marshal([]any{m.joinRef, m.ref, m.topic, m.event, m.payload})
if err != nil {
return nil, err
}
return data, nil
}
func deserializeJSON(data []byte, msg *message) error {
m := message{}
slice := []any{&m.joinRef, &m.ref, &m.topic, &m.event, &m.payload}
err := json.Unmarshal(data, &slice)
if err != nil {
return err
}
*msg = m
return nil
}

View file

@ -0,0 +1,99 @@
package gophx
import (
"context"
"fmt"
"nhooyr.io/websocket"
)
type Socket struct {
sendChan chan []byte
websocket *websocket.Conn
channels map[string]*Channel
}
func Connect(ctx context.Context) (*Socket, error) {
serverUrl := "ws://localhost:4000/agent/websocket?vsn=2.0.0"
c, _, err := websocket.Dial(ctx, serverUrl, nil)
if err != nil {
return nil, err
}
socket := Socket{
sendChan: make(chan []byte),
channels: make(map[string]*Channel),
websocket: c,
}
go socket.readMessages(ctx)
go writeMessages(ctx, socket.sendChan, c)
return &socket, nil
}
func (s *Socket) addChannel(ch *Channel) {
s.channels[ch.topic] = ch
}
func (s *Socket) send(data []byte) {
s.sendChan <- data
}
func (s *Socket) readMessages(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
typ, data, err := s.websocket.Read(ctx)
if err != nil {
// TODO: try to reconnect
fmt.Println("socket closed?", err)
return
}
switch typ {
case websocket.MessageBinary:
fmt.Println("received binary", data)
case websocket.MessageText:
msg := message{}
err := deserializeJSON(data, &msg)
if err != nil {
fmt.Println(err)
}
s.sendToChannel(msg)
}
}
}
}
func (s *Socket) sendToChannel(msg message) {
ch, ok := s.channels[msg.topic]
if !ok {
return
}
ch.handleMessage(msg)
}
func writeMessages(ctx context.Context, sendChan chan []byte, ws *websocket.Conn) {
for {
select {
case message, ok := <-sendChan:
if !ok {
fmt.Println("sending channel closed")
return
}
err := ws.Write(ctx, websocket.MessageText, message)
if err != nil {
fmt.Println(err)
}
}
}
}

View file

@ -6,7 +6,6 @@ defmodule Prymn.MixProject do
app: :prymn, app: :prymn,
version: "0.1.0", version: "0.1.0",
elixir: "~> 1.15", elixir: "~> 1.15",
compilers: [:proto | Mix.compilers()],
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
config_path: Path.expand("app/config/config.exs", __DIR__), config_path: Path.expand("app/config/config.exs", __DIR__),
test_paths: ["app/test"], test_paths: ["app/test"],
@ -51,10 +50,8 @@ defmodule Prymn.MixProject do
{:jason, "~> 1.2"}, {:jason, "~> 1.2"},
{:dns_cluster, "~> 0.1.1"}, {:dns_cluster, "~> 0.1.1"},
{:plug_cowboy, "~> 2.5"}, {:plug_cowboy, "~> 2.5"},
{:grpc, "~> 0.7"},
{:protobuf, "~> 0.12.0"},
{:google_protos, "~> 0.3.0"},
{:oban, "~> 2.17"}, {:oban, "~> 2.17"},
{:gnat, "~> 1.7"},
# Test # Test
{:floki, ">= 0.30.0", only: :test}, {:floki, ">= 0.30.0", only: :test},
@ -66,7 +63,6 @@ defmodule Prymn.MixProject do
{:tailwind_formatter, "~> 0.3.6", runtime: Mix.env() == :dev}, {:tailwind_formatter, "~> 0.3.6", runtime: Mix.env() == :dev},
{:phoenix_live_reload, "~> 1.2", only: :dev}, {:phoenix_live_reload, "~> 1.2", only: :dev},
{:dialyxir, "~> 1.4", only: [:dev], runtime: false}, {:dialyxir, "~> 1.4", only: [:dev], runtime: false},
{:prymn_proto_compiler, path: "proto_compiler", runtime: false}
] ]
end end

View file

@ -13,6 +13,7 @@
"dns_cluster": {:hex, :dns_cluster, "0.1.1", "73b4b2c3ec692f8a64276c43f8c929733a9ab9ac48c34e4c0b3d9d1b5cd69155", [:mix], [], "hexpm", "03a3f6ff16dcbb53e219b99c7af6aab29eb6b88acf80164b4bd76ac18dc890b3"}, "dns_cluster": {:hex, :dns_cluster, "0.1.1", "73b4b2c3ec692f8a64276c43f8c929733a9ab9ac48c34e4c0b3d9d1b5cd69155", [:mix], [], "hexpm", "03a3f6ff16dcbb53e219b99c7af6aab29eb6b88acf80164b4bd76ac18dc890b3"},
"ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"}, "ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"},
"ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"}, "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
"ed25519": {:hex, :ed25519, "1.4.1", "479fb83c3e31987c9cad780e6aeb8f2015fb5a482618cdf2a825c9aff809afc4", [:mix], [], "hexpm", "0dacb84f3faa3d8148e81019ca35f9d8dcee13232c32c9db5c2fb8ff48c80ec7"},
"elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"}, "elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"}, "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
@ -21,15 +22,15 @@
"finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"}, "finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"},
"floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"}, "floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"},
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
"google_protos": {:hex, :google_protos, "0.3.0", "15faf44dce678ac028c289668ff56548806e313e4959a3aaf4f6e1ebe8db83f4", [:mix], [{:protobuf, "~> 0.10", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1f6b7fb20371f72f418b98e5e48dae3e022a9a6de1858d4b254ac5a5d0b4035f"}, "gnat": {:hex, :gnat, "1.7.1", "491144f9c3aec00e9941d69538e2fd2836271e220315c8d2d87907c20ca7abc8", [:mix], [{:cowlib, "~> 2.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:nkeys, "~> 0.2", [hex: :nkeys, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5629088d9bdb16d982eb48fd431cf6c5a71e9b026281781983501237ab5b911"},
"grpc": {:hex, :grpc, "0.7.0", "a86eab356b0b84406b526786a947ca50e9b9eae87108c873b51e321f8a71e8ed", [:mix], [{:cowboy, "~> 2.10", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowlib, "~> 2.12", [hex: :cowlib, repo: "hexpm", optional: false]}, {:gun, "~> 2.0", [hex: :gun, repo: "hexpm", optional: false]}, {:mint, "~> 1.5", [hex: :mint, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "632a9507da8d3c12b112b197db4d60da3c95bad02594d37711eeb622d032f254"},
"gun": {:hex, :gun, "2.0.1", "160a9a5394800fcba41bc7e6d421295cf9a7894c2252c0678244948e3336ad73", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "a10bc8d6096b9502205022334f719cc9a08d9adcfbfc0dbee9ef31b56274a20b"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"}, "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
"nkeys": {:hex, :nkeys, "0.2.2", "b1ab3324ed4f3a2c9658d7e80feeef86b4d15fbfd12ca5c8cf068289f582fcfa", [:mix], [{:ed25519, "~> 1.3", [hex: :ed25519, repo: "hexpm", optional: false]}], "hexpm", "3578802427b8d1d11ea6dd785c2ab774f527e2c3e449e67bd34612ab71ca471d"},
"oban": {:hex, :oban, "2.17.1", "42d6221a1c17b63d81c19e3bad9ea82b59e39c47c1f9b7670ee33628569a449b", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c02686ada7979b00e259c0efbafeae2749f8209747b3460001fe695c5bdbeee6"}, "oban": {:hex, :oban, "2.17.1", "42d6221a1c17b63d81c19e3bad9ea82b59e39c47c1f9b7670ee33628569a449b", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c02686ada7979b00e259c0efbafeae2749f8209747b3460001fe695c5bdbeee6"},
"phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"}, "phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"},
@ -43,7 +44,6 @@
"plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
"postgrex": {:hex, :postgrex, "0.17.4", "5777781f80f53b7c431a001c8dad83ee167bcebcf3a793e3906efff680ab62b3", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "6458f7d5b70652bc81c3ea759f91736c16a31be000f306d3c64bcdfe9a18b3cc"}, "postgrex": {:hex, :postgrex, "0.17.4", "5777781f80f53b7c431a001c8dad83ee167bcebcf3a793e3906efff680ab62b3", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "6458f7d5b70652bc81c3ea759f91736c16a31be000f306d3c64bcdfe9a18b3cc"},
"protobuf": {:hex, :protobuf, "0.12.0", "58c0dfea5f929b96b5aa54ec02b7130688f09d2de5ddc521d696eec2a015b223", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "75fa6cbf262062073dd51be44dd0ab940500e18386a6c4e87d5819a58964dc45"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"swoosh": {:hex, :swoosh, "1.14.3", "949e6bf6dd469449238a94ec6f19ec10b63fc8753de7f3ebe3d3aeaf772f4c6b", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6c565103fc8f086bdd96e5c948660af8e20922b7a90a75db261f06a34f805c8b"}, "swoosh": {:hex, :swoosh, "1.14.3", "949e6bf6dd469449238a94ec6f19ec10b63fc8753de7f3ebe3d3aeaf772f4c6b", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6c565103fc8f086bdd96e5c948660af8e20922b7a90a75db261f06a34f805c8b"},
"tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"}, "tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"},

View file

@ -6,6 +6,4 @@ final: prev: {
elixir = prev.beam.packages.erlang_26.elixir_1_15; elixir = prev.beam.packages.erlang_26.elixir_1_15;
elixir-ls = prev.beam.packages.erlang_26.elixir-ls.override { elixir = final.elixir; }; elixir-ls = prev.beam.packages.erlang_26.elixir-ls.override { elixir = final.elixir; };
protoc-gen-elixir = prev.callPackage ./protoc-gen-elixir.nix { elixir = final.elixir; };
} }

View file

@ -1,28 +0,0 @@
{ elixir, beamPackages, fetchFromGitHub }:
beamPackages.mixRelease rec {
pname = "protoc-gen-elixir";
version = "v0.12.0";
inherit elixir;
src = fetchFromGitHub {
owner = "elixir-protobuf";
repo = "protobuf";
rev = "ce9a031a5cae97336d4674670d313d54f1f80bf6";
sha256 = "wLU3iM9jI/Zc96/HfPUjNvjteGryWos6IobIb/4zqpw=";
};
mixFodDeps = beamPackages.fetchMixDeps {
pname = "mix-deps-${pname}";
inherit src version;
sha256 = "H7yiBHoxuiqWcNbWwPU5X0Nnv8f6nM8z/ZAfZAGPZjE=";
};
installPhase = ''
mix escript.build
mkdir -p $out/bin
mv ./protoc-gen-elixir $out/bin
'';
}

View file

@ -1,47 +0,0 @@
{ writeShellApplication, postgresql, ... }:
{
prymn_db = writeShellApplication {
name = "prymn_db";
runtimeInputs = [ postgresql ];
text = ''
#!/usr/bin/env bash
set -e
export PGDATA=$PROJECT_ROOT_DIR/.db
export PGHOST=/tmp
export DB_LOG=$PROJECT_ROOT_DIR/.db/log
start_db() {
if [ ! -d "$PGDATA" ]; then
initdb "$PGDATA" --auth=trust
fi
if ! pg_ctl status >/dev/null; then
pg_ctl start -l "$DB_LOG" -o "-c unix_socket_directories=$PGHOST"
echo "starting your dev database..."
fi
user_exists=$(psql --csv -t -d postgres -c "SELECT count(*) FROM pg_user WHERE usename='postgres'")
if [ "$user_exists" != "1" ]; then
createuser -s -h "$PGHOST" postgres
fi
}
command="''${1-default}"
case $command in
shell)
psql -U postgres
exit 0
;;
start | default | *)
start_db
exit 0
;;
esac
'';
};
}

View file

@ -1,92 +0,0 @@
syntax = "proto3";
import "google/protobuf/empty.proto";
package prymn;
message SystemHealth {
// Comma-separated statuses
string status = 1;
}
message TaskHealth {
string started_on = 1;
float progress = 2;
}
message HealthResponse {
string version = 1;
SystemHealth system = 2;
map<string, TaskHealth> tasks = 3;
}
message SysInfoResponse {
message Cpu {
uint64 freq_mhz = 1;
float usage = 2;
}
message Disk {
string name = 1;
uint64 total_bytes = 2;
uint64 avail_bytes = 3;
string mount_point = 4;
}
uint64 uptime = 1;
string hostname = 2;
string os = 3;
uint64 mem_total_bytes = 4;
uint64 mem_avail_bytes = 5;
uint64 swap_total_bytes = 6;
uint64 swap_free_bytes = 7;
repeated Cpu cpus = 8;
repeated Disk disks = 9;
uint32 updates_available = 10;
}
message ExecRequest {
string user = 1;
string program = 2;
repeated string args = 3;
}
message ExecResponse {
oneof out {
string stdout = 1;
string stderr = 2;
string error = 3;
int32 exit_code = 4;
}
}
message SysUpdateRequest {
bool dry_run = 1;
}
message SysUpdateResponse {
string output = 1;
int32 progress = 2;
}
message TerminalRequest {
message Resize {
uint32 rows = 1;
uint32 cols = 2;
}
bytes input = 1;
optional Resize resize = 2;
}
message TerminalResponse {
bytes output = 1;
}
service Agent {
rpc Health(google.protobuf.Empty) returns (stream HealthResponse);
rpc Exec(ExecRequest) returns (stream ExecResponse);
rpc Terminal(stream TerminalRequest) returns (stream TerminalResponse);
rpc GetSysInfo(google.protobuf.Empty) returns (SysInfoResponse);
rpc SysUpdate(SysUpdateRequest) returns (stream SysUpdateResponse);
}

View file

@ -1,4 +0,0 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

View file

@ -1,26 +0,0 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
proto-*.tar
# Temporary files, for example, from tests.
/tmp/

View file

@ -1,34 +0,0 @@
defmodule Mix.Tasks.Compile.Proto do
use Mix.Task.Compiler
@manifest "compile.proto"
@impl true
def run(_args) do
output = "./app/lib/prymn_proto"
sources = Path.wildcard("proto/*.proto")
targets = Path.wildcard(output <> "/*.pb.ex")
if Mix.Utils.stale?(sources, targets) do
{_, 0} = do_protoc(sources, output)
end
:ok
end
@impl true
def manifests(), do: [manifest()]
defp manifest(), do: Path.join(Mix.Project.manifest_path(), @manifest)
defp do_protoc(sources, output) do
System.cmd(
"protoc",
[
"--elixir_out=plugins=grpc:" <> output,
"--elixir_opt=package_prefix=prymn_proto",
"-I",
"proto"
] ++ sources
)
end
end

View file

@ -1,22 +0,0 @@
defmodule PrymnProtoCompiler.MixProject do
use Mix.Project
def project do
[
app: :prymn_proto_compiler,
version: "0.1.0",
elixir: "~> 1.15",
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[]
end
end