init: and so there was the tree
This commit is contained in:
commit
d0c1fa3719
23 changed files with 519 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
use flake
|
||||
4
.formatter.exs
Normal file
4
.formatter.exs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
### Elixir ###
|
||||
/_build
|
||||
/cover
|
||||
/deps
|
||||
/doc
|
||||
/.fetch
|
||||
erl_crash.dump
|
||||
*.ez
|
||||
*.beam
|
||||
/config/*.secret.exs
|
||||
.elixir_ls/
|
||||
.expert/
|
||||
### End of Elixir ###
|
||||
|
||||
### Nix ###
|
||||
# Ignore build outputs from performing a nix-build or `nix build` command
|
||||
result
|
||||
result-*
|
||||
|
||||
# Ignore automatically generated direnv output
|
||||
.direnv
|
||||
### End of Nix ###
|
||||
|
||||
3
README.md
Normal file
3
README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Sunbeam
|
||||
|
||||
An IRCd in Elixir, made as an experiment by `teesh3rt`.
|
||||
77
flake.lock
generated
Normal file
77
flake.lock
generated
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1769996383,
|
||||
"narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"import-tree": {
|
||||
"locked": {
|
||||
"lastModified": 1771045967,
|
||||
"narHash": "sha256-oYO4poyw0Sb/db2PigqugMlDwsvwLg6CSpFrMUWxA3Q=",
|
||||
"owner": "vic",
|
||||
"repo": "import-tree",
|
||||
"rev": "c968d3b54d12cf5d9c13f16f7c545a06c9d1fde6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "vic",
|
||||
"repo": "import-tree",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1771008912,
|
||||
"narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a82ccc39b39b621151d6732718e3e250109076fa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1769909678,
|
||||
"narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "72716169fe93074c333e8d0173151350670b824c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts",
|
||||
"import-tree": "import-tree",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
13
flake.nix
Normal file
13
flake.nix
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
description = "A very basic flake";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
import-tree.url = "github:vic/import-tree";
|
||||
};
|
||||
|
||||
outputs = inputs:
|
||||
inputs.flake-parts.lib.mkFlake {inherit inputs;}
|
||||
(inputs.import-tree ./packages/nix);
|
||||
}
|
||||
17
lib/sunbeam/acceptor_supervisor.ex
Normal file
17
lib/sunbeam/acceptor_supervisor.ex
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
defmodule Sunbeam.AcceptorSupervisor do
|
||||
use Supervisor
|
||||
|
||||
def start_link(socket) do
|
||||
Supervisor.start_link(__MODULE__, socket, name: __MODULE__)
|
||||
end
|
||||
|
||||
def init(socket) do
|
||||
children =
|
||||
for i <- 1..20 do
|
||||
Supervisor.child_spec({Sunbeam.TcpAcceptor, socket}, id: {Sunbeam.TcpAcceptor, i})
|
||||
end
|
||||
|
||||
options = [strategy: :one_for_all, name: __MODULE__]
|
||||
Supervisor.init(children, options)
|
||||
end
|
||||
end
|
||||
25
lib/sunbeam/application.ex
Normal file
25
lib/sunbeam/application.ex
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
defmodule Sunbeam.Application do
|
||||
use Application
|
||||
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
# Channels
|
||||
{Registry, keys: :unique, name: Sunbeam.ChannelRegistry},
|
||||
Sunbeam.ChannelSupervisor,
|
||||
|
||||
# Users
|
||||
{Registry, keys: :unique, name: Sunbeam.UserRegistry},
|
||||
Sunbeam.UserSupervisor,
|
||||
|
||||
# Sockets
|
||||
Sunbeam.SocketSupervisor,
|
||||
|
||||
# Monitoring
|
||||
Sunbeam.MonitoringWorker
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: Sunbeam.RootSupervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
end
|
||||
14
lib/sunbeam/channel.ex
Normal file
14
lib/sunbeam/channel.ex
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
defmodule Sunbeam.Channel do
|
||||
use GenServer, restart: :transient
|
||||
|
||||
def start_link(name) do
|
||||
GenServer.start_link(__MODULE__, name,
|
||||
name: {:via, Registry, {Sunbeam.ChannelRegistry, name}}
|
||||
)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_args) do
|
||||
{:ok, %{users: MapSet.new()}}
|
||||
end
|
||||
end
|
||||
21
lib/sunbeam/channel_supervisor.ex
Normal file
21
lib/sunbeam/channel_supervisor.ex
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
defmodule Sunbeam.ChannelSupervisor do
|
||||
use DynamicSupervisor
|
||||
|
||||
# NOTE: Also creates a new channel if it dosent exist
|
||||
def get(name) do
|
||||
case DynamicSupervisor.start_child(__MODULE__, {Sunbeam.Channel, name}) do
|
||||
{:ok, pid} -> {:ok, pid}
|
||||
{:error, {:already_started, pid}} -> {:ok, pid}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
def start_link(init) do
|
||||
DynamicSupervisor.start_link(__MODULE__, init, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_init) do
|
||||
DynamicSupervisor.init(strategy: :one_for_one)
|
||||
end
|
||||
end
|
||||
32
lib/sunbeam/connection.ex
Normal file
32
lib/sunbeam/connection.ex
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
defmodule Sunbeam.Connection do
|
||||
use GenServer, restart: :temporary
|
||||
|
||||
require Logger
|
||||
|
||||
def start_link(socket) do
|
||||
GenServer.start_link(__MODULE__, socket)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(socket) do
|
||||
{:ok, user} = Sunbeam.UserSupervisor.create_user(self())
|
||||
Process.link(user)
|
||||
{:ok, %{user: user, socket: socket}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:tcp, socket, data}, state) do
|
||||
:inet.setopts(socket, active: :once)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:tcp_closed, socket}, state) do
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:tcp_error, socket, reason}, state) do
|
||||
{:stop, reason, state}
|
||||
end
|
||||
end
|
||||
16
lib/sunbeam/connection_supervisor.ex
Normal file
16
lib/sunbeam/connection_supervisor.ex
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
defmodule Sunbeam.ConnectionSupervisor do
|
||||
use DynamicSupervisor
|
||||
|
||||
def start_link(init) do
|
||||
DynamicSupervisor.start_link(__MODULE__, init, name: __MODULE__)
|
||||
end
|
||||
|
||||
def create_conn_from(client_socket) do
|
||||
DynamicSupervisor.start_child(__MODULE__, {Sunbeam.Connection, client_socket})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_arg) do
|
||||
DynamicSupervisor.init(strategy: :one_for_one)
|
||||
end
|
||||
end
|
||||
86
lib/sunbeam/debug/supervision_tree.ex
Normal file
86
lib/sunbeam/debug/supervision_tree.ex
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
defmodule Sunbeam.Debug.SupervisionTree do
|
||||
@moduledoc """
|
||||
Prints a supervision tree like the `tree` command with colors.
|
||||
Depth-based coloring: fire-red at root, fading to ash-gray at deepest levels.
|
||||
DynamicSupervisor children registered in a Registry show their name.
|
||||
Limits children per node to avoid crashing the logger.
|
||||
"""
|
||||
|
||||
@colors [
|
||||
# fire red
|
||||
"\e[38;5;196m",
|
||||
# orange
|
||||
"\e[38;5;202m",
|
||||
# yellow
|
||||
"\e[38;5;226m",
|
||||
# light yellow
|
||||
"\e[38;5;190m",
|
||||
# light green
|
||||
"\e[38;5;145m",
|
||||
# cyan
|
||||
"\e[38;5;111m",
|
||||
# ash gray
|
||||
"\e[38;5;250m"
|
||||
]
|
||||
|
||||
@reset "\e[0m"
|
||||
@max_children 20
|
||||
|
||||
# Public entry point
|
||||
def print_tree(supervisor), do: print_tree(supervisor, "", 0)
|
||||
|
||||
# Internal recursive function with depth
|
||||
defp print_tree(supervisor, prefix, depth) do
|
||||
children = get_children(supervisor)
|
||||
color = color_for_depth(depth)
|
||||
|
||||
IO.puts("#{prefix}#{color}#{inspect(supervisor)}#{@reset}")
|
||||
|
||||
Enum.take(children, @max_children)
|
||||
|> Enum.with_index()
|
||||
|> Enum.each(fn {{id, child_pid, type, _modules}, index} ->
|
||||
last = index == min(length(children), @max_children) - 1
|
||||
branch = if last, do: "└─ ", else: "├─ "
|
||||
sub_prefix = if last, do: prefix <> " ", else: prefix <> "│ "
|
||||
|
||||
# Lookup registry name for dynamic workers
|
||||
registry_name = get_registry_name(child_pid)
|
||||
|
||||
display_id =
|
||||
cond do
|
||||
id != :undefined -> inspect(id)
|
||||
type == :worker and registry_name != nil -> inspect(registry_name)
|
||||
true -> inspect(child_pid)
|
||||
end
|
||||
|
||||
node_color = color_for_depth(depth + 1)
|
||||
IO.puts("#{prefix}#{branch}#{node_color}#{display_id} (#{type})#{@reset}")
|
||||
|
||||
if type == :supervisor do
|
||||
print_tree(child_pid, sub_prefix, depth + 1)
|
||||
end
|
||||
end)
|
||||
|
||||
if length(children) > @max_children do
|
||||
IO.puts("#{prefix}│ ... (#{length(children) - @max_children} more children)")
|
||||
end
|
||||
end
|
||||
|
||||
defp get_children(pid) do
|
||||
case :supervisor.which_children(pid) do
|
||||
children when is_list(children) -> children
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp get_registry_name(pid) when is_pid(pid) do
|
||||
case Registry.keys(Sunbeam.ChannelRegistry, pid) do
|
||||
[name] -> name
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp color_for_depth(depth) do
|
||||
Enum.at(@colors, min(depth, length(@colors) - 1))
|
||||
end
|
||||
end
|
||||
5
lib/sunbeam/irc/codes.ex
Normal file
5
lib/sunbeam/irc/codes.ex
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
defmodule Sunbeam.Irc.Codes do
|
||||
@rpl_welcome "001"
|
||||
|
||||
def rpl_welcome(), do: @rpl_welcome
|
||||
end
|
||||
22
lib/sunbeam/monitoring_worker.ex
Normal file
22
lib/sunbeam/monitoring_worker.ex
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
defmodule Sunbeam.MonitoringWorker do
|
||||
use GenServer
|
||||
|
||||
def start_link(arg) do
|
||||
GenServer.start_link(__MODULE__, arg)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_arg) do
|
||||
send(self(), :monitor)
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:monitor, state) do
|
||||
IO.write("\e[H\e[2J")
|
||||
Sunbeam.Debug.SupervisionTree.print_tree(Sunbeam.RootSupervisor)
|
||||
Process.send_after(self(), :monitor, 1000 * 1)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
26
lib/sunbeam/socket_supervisor.ex
Normal file
26
lib/sunbeam/socket_supervisor.ex
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
defmodule Sunbeam.SocketSupervisor do
|
||||
use Supervisor
|
||||
|
||||
def start_link(init_arg) do
|
||||
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_arg) do
|
||||
{:ok, socket} =
|
||||
:gen_tcp.listen(6667, [
|
||||
:binary,
|
||||
packet: 0,
|
||||
active: false,
|
||||
reuseaddr: true
|
||||
])
|
||||
|
||||
children = [
|
||||
Sunbeam.ConnectionSupervisor,
|
||||
{Sunbeam.AcceptorSupervisor, socket}
|
||||
]
|
||||
|
||||
options = [strategy: :one_for_one]
|
||||
Supervisor.init(children, options)
|
||||
end
|
||||
end
|
||||
35
lib/sunbeam/tcp_acceptor.ex
Normal file
35
lib/sunbeam/tcp_acceptor.ex
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
defmodule Sunbeam.TcpAcceptor do
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
def start_link(listen_socket) do
|
||||
GenServer.start_link(__MODULE__, listen_socket)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(listen_socket) do
|
||||
send(self(), :accept)
|
||||
{:ok, listen_socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:accept, listen_socket) do
|
||||
case :gen_tcp.accept(listen_socket, :infinity) do
|
||||
{:ok, client_socket} ->
|
||||
{:ok, conn} = Sunbeam.ConnectionSupervisor.create_conn_from(client_socket)
|
||||
:gen_tcp.controlling_process(client_socket, conn)
|
||||
:inet.setopts(client_socket, active: :once)
|
||||
send(self(), :accept)
|
||||
{:noreply, listen_socket}
|
||||
|
||||
{:error, :closed} ->
|
||||
{:stop, :normal, listen_socket}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Accept failed: #{inspect(reason)}")
|
||||
send(self(), :accept)
|
||||
{:noreply, listen_socket}
|
||||
end
|
||||
end
|
||||
end
|
||||
30
lib/sunbeam/user.ex
Normal file
30
lib/sunbeam/user.ex
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
defmodule Sunbeam.User do
|
||||
use GenServer, restart: :transient
|
||||
|
||||
def start_link(connection) do
|
||||
GenServer.start_link(__MODULE__, connection)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(connection) do
|
||||
Process.monitor(connection)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
# NOTE: This is not a handle to a raw socket.
|
||||
# What are we, Python???
|
||||
connection: connection,
|
||||
|
||||
# NICK & USER, CAP, JOIN
|
||||
# Can either be :registration, :negotiation or :ready
|
||||
phase: :registration,
|
||||
nick: nil,
|
||||
user: nil
|
||||
}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:DOWN, _, _, _, _}, state) do
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
end
|
||||
16
lib/sunbeam/user_supervisor.ex
Normal file
16
lib/sunbeam/user_supervisor.ex
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
defmodule Sunbeam.UserSupervisor do
|
||||
use DynamicSupervisor
|
||||
|
||||
def start_link(init) do
|
||||
DynamicSupervisor.start_link(__MODULE__, init, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_init) do
|
||||
DynamicSupervisor.init(strategy: :one_for_one)
|
||||
end
|
||||
|
||||
def create_user(conn) do
|
||||
DynamicSupervisor.start_child(__MODULE__, {Sunbeam.User, conn})
|
||||
end
|
||||
end
|
||||
30
mix.exs
Normal file
30
mix.exs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
defmodule Sunbeam.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :sunbeam,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.18",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help compile.app" to learn about applications.
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger, :observer_cli],
|
||||
mod: {Sunbeam.Application, []}
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help deps" to learn about dependencies.
|
||||
defp deps do
|
||||
[
|
||||
# {:dep_from_hexpm, "~> 0.3.0"},
|
||||
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||
{:observer_cli, "~> 1.7"}
|
||||
]
|
||||
end
|
||||
end
|
||||
4
mix.lock
Normal file
4
mix.lock
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
%{
|
||||
"observer_cli": {:hex, :observer_cli, "1.8.6", "65547365b56532dabb42c91de90af5b55894218a986ae47e1db47ecb8c979d8f", [:mix, :rebar3], [{:recon, "~> 2.5.6", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "ea743c6477bc4799f215f8ef7cd5eee1536dcdda311fe2bd204e88e7c1e6a11a"},
|
||||
"recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"},
|
||||
}
|
||||
14
packages/nix/shell.nix
Normal file
14
packages/nix/shell.nix
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{...}: {
|
||||
perSystem = {pkgs, ...}: {
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
beamPackages.elixir
|
||||
beamPackages.erlang
|
||||
rebar3
|
||||
hex
|
||||
openssl
|
||||
git
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
5
packages/nix/systems.nix
Normal file
5
packages/nix/systems.nix
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{...}: {
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue