summaryrefslogtreecommitdiff
path: root/presentation
diff options
context:
space:
mode:
authorLizzy Hunt <loganhunt@simponic.xyz>2023-02-23 19:52:25 -0700
committerGitHub <noreply@github.com>2023-02-23 19:52:25 -0700
commit829deb33e3fbe70f407b482faf63338ee73d3f55 (patch)
treed7a55bf9679155a660cf708ab7fad235e0d9baa4 /presentation
parent2e45ff0a7a1f0804656d07977a23f803eb3b04f7 (diff)
downloadchessh-829deb33e3fbe70f407b482faf63338ee73d3f55.tar.gz
chessh-829deb33e3fbe70f407b482faf63338ee73d3f55.zip
Presentation (#17)
* Initial presentation * More pres * More presentation * Presentation link
Diffstat (limited to 'presentation')
-rw-r--r--presentation/architecture.pngbin0 -> 532468 bytes
-rw-r--r--presentation/chessh.org328
-rw-r--r--presentation/pis.jpegbin0 -> 316106 bytes
-rw-r--r--presentation/tic_tac_toe.exs355
4 files changed, 683 insertions, 0 deletions
diff --git a/presentation/architecture.png b/presentation/architecture.png
new file mode 100644
index 0000000..f4dd6f7
--- /dev/null
+++ b/presentation/architecture.png
Binary files differ
diff --git a/presentation/chessh.org b/presentation/chessh.org
new file mode 100644
index 0000000..ff13cfd
--- /dev/null
+++ b/presentation/chessh.org
@@ -0,0 +1,328 @@
+#+TITLE: Practicing Elixir by Building Concurrent, Distributed, Multiplayer Games in the Terminal
+#+AUTHOR: Lizzy Hunt (Simponic)
+#+STARTUP: fold inlineimages
+
+* Reminder: linux.usu.edu
+This meeting should be being streamed live at [[https://linux.usu.edu/streams]].
+
+* Introduction
+#+BEGIN_SRC elixir
+ defmodule Hello do
+ def hello() do
+ "Hello, Linux Club!"
+ |> IO.puts
+ end
+ end
+
+ Hello.hello()
+#+END_SRC
+
+** CheSSH
+CheSSH is a multiplayer distributed game of chess over SSH - let's take a quick look before diving into Elixir!
+
+[[https://chessh.linux.usu.edu]]
+
+* Elixir - What You Need
+Elixir is a self-proclaimed "dynamic, functional language for building scalable and maintainable applications".
+
+** Basic Data Types
+1. ~int~'s, ~bool~'s, ~string~'s are all here
+ + ~1~, ~true~, ~"Hello"~
+2. Atoms: prefixed with ":" are named constants whose name is their value, similar to symbols in LISP
+ + ~:x~, ~:three~
+4. Maps: regular key-value store; keys can be literally anything, including other maps
+ + ~%{%{a: 1}: 2, %{a: 2}: :an_atom}~
+5. Lists: lists are singly-linked elements of "stuff"
+ + ~[1,2,3]~, ~[]~, ~[1, [2, :three, %{}]]~
+6. Tuples: tuples are fixed-size collections of "stuff"
+ + ~{1,2,3}~, ~{1, {2, 3}}~
+
+** Pattern Matching
+The match operator "=" does not mean its convential meaning of assignment, but instead an assertion of equivalence. This gives way to a unique
+feature of Elixir - pattern matching (similar to that found in Rust's ~match~ or Scala's ~case~).
+
+With pattern matching we can access data from complex structures declaratively.
+
+For example:
+#+BEGIN_SRC elixir
+ [head | tail] = [1,2,3]
+ %{a: a_value} = %{a: 10}
+ {:ok, result} = {:ok, 2}
+ [head, tail, a_value, result]
+#+END_SRC
+
+And will raise an exception when the pattern cannot match:
+
+#+BEGIN_SRC elixir
+ %{a: a_value} = %{b: 10}
+#+END_SRC
+
+*** Error Handling
+Functions that can error will typically return a two-tuple, the first element of which is either the atom ~:ok~ or ~:error~, and the second is the
+error info or value.
+
+For many scenarios, the fact that a failed pattern match raises an exception is enough information to know we shouldn't execute further.
+#+BEGIN_SRC elixir
+ defmodule Sequences do
+ def fib(n) when n < 0, do: {:error, :too_small}
+ def fib(n) when n <= 1, do: {:ok, n}
+ def fib(n) when n > 1 do
+ {:ok, n1} = fib(n-1)
+ {:ok, n2} = fib(n-2)
+
+ {:ok, n1 + n2}
+ end
+ end
+
+ {:ok, f10} = Sequences.fib(10)
+ {:ok, fn1} = Sequences.fib(-1)
+
+ IO.puts(fn1)
+#+END_SRC
+
+But sometimes we do want to capture that error information! In this case, we use ~case~!
+
+#+BEGIN_SRC elixir
+ case Sequences.fib(-1) do
+ {:ok, val} -> val
+ {:error, err} ->
+ IO.puts("Ran into :error #{inspect(err)}")
+ 0
+ end
+#+END_SRC
+
+** Piping
+Elixir's pipe operator ~|>~ allows programmers to easily write statements as a composition of functions. It simply takes the value of the
+function on the left, and passes it as the first argument to the function on the right.
+
+For example, to find the length of the longest string in a list of strings:
+#+BEGIN_SRC elixir
+ ["Hello, world", "Another string", "Where are all these strings coming from"]
+ |> Enum.map(&String.length/1)
+ |> Enum.max()
+#+END_SRC
+
+** Meta-programming
+Akin to my favorite language of all time, LISP, Elixir provides a way to interact directly with code as data (and thus the AST) via a powerful macro system.
+
+However, they are not as elegant, and for that reason, Chris McCord suggests in his book "Metaprogramming Elixir":
+
+#+BEGIN_QUOTE
+Rule 1 : Don't Write Macros
+#+END_QUOTE
+
+The main reasoning is that it becomes difficult to debug, and hides too much from the user. These are fine trade-offs when you're working alone.
+
+*** when-prime the functional way
+#+BEGIN_SRC elixir
+ defmodule Prime do
+ def is_prime(2), do: true
+ def is_prime(n) when rem(n, 2) == 0 or n <= 1, do: false
+ def is_prime(n) do
+ is_prime_helper(n, 3)
+ end
+
+ defp is_prime_helper(n, i) when i * i > n, do: true
+ defp is_prime_helper(n, i) when rem(n, i) == 0, do: false
+ defp is_prime_helper(n, i) do
+ is_prime_helper(n, i + 2)
+ end
+ end
+#+END_SRC
+
+#+BEGIN_SRC elixir
+ when_prime_do = fn n, when_true, when_false ->
+ if Prime.is_prime(n) do
+ when_true.()
+ else
+ when_false.()
+ end
+ end
+
+ when_prime_do.(10, fn -> "10 is prime" end, fn -> "10 is not prime" end)
+#+END_SRC
+
+*** when-prime the metaprogramming way
+#+BEGIN_SRC elixir
+ defmodule When do
+ defmacro prime(n, do: true_body, else: false_body) do
+ quote do
+ if Prime.is_prime(unquote(n)), do: unquote(true_body), else: unquote(false_body)
+ end
+ end
+ end
+
+ require When
+ When.prime 10, do: "10 is prime", else: "10 is not prime"
+#+END_SRC
+
+*** Real-world use-case: ~use~
+One such use case for macros (besides those covered previously in my LISP presentation) is to emulate module "inheritance" to share functions.
+
+We can think of a module in Elixir as a set of functions. Then, we can perform unions of modules by the ~use~ macros.
+
+Additionally, with ~behaviours~ we can define callbacks to implement in each unioned module.
+
+#+BEGIN_SRC elixir
+ defmodule Animal do
+ @callback noise() :: String.t()
+
+ defmacro __using__(_opts) do
+ quote do
+ @behaviour Animal
+
+ def speak() do
+ IO.puts("#{__MODULE__} says #{noise()}")
+ end
+ end
+ end
+ end
+
+ defmodule Dog do
+ use Animal
+
+ def noise() do
+ "Bark"
+ end
+ end
+
+ defmodule Cat do
+ use Animal
+
+ def noise() do
+ "Meow"
+ end
+ end
+
+ Cat.speak()
+ Dog.speak()
+#+END_SRC
+
+* Elixir - Concurrency
+Elixir is built on top of (and completely interoperable with) Erlang - a language developed to build massively fault-tolerant systems in the 80's
+for large telephone exchanges with hundreds of thousands of users.
+
+You can imagine (if you look past the many problems with this statement), Elixir and Erlang to be analogous to Python and C, respectively - but
+without the massive performance penalty.
+
+** The BEAM
+The BEAM powers Elixir's concurrency magic; by running a VM executing Erlang bytecode that holds one OS thread per core,
+and a separate process scheduler (and queue) on each.
+
+Imagine an army of little goblins, and you give each a todo list. The goblins then go complete the tasks in the order best
+suited for them, and have the added benefit that they can talk to each other.
+
+** Concurrency - Demo!
+Here we will open up two terminals: one running an Elixir REPL on my machine, and another to SSH into my android:
+
+#+BEGIN_SRC python
+ import subprocess
+ import string
+ import random
+ cookie = ''.join(random.choices(string.ascii_uppercase +
+ string.digits, k=32))
+ host = "host"
+ android = "a02364151-23.bluezone.usu.edu"
+
+ h = subprocess.Popen(f"alacritty -e rlwrap --always-readline iex --name lizzy@{host} --cookie {cookie}".split())
+ a = subprocess.Popen(f"alacritty -e ssh u0_a308@{android} -p 2222 rlwrap --always-readline iex --name android@{android} --cookie {cookie}".split())
+#+END_SRC
+
+#+BEGIN_SRC elixir
+ defmodule SpeakServer do
+ @sleep_between_msg 2000
+
+ def loop(queue \\ []) do
+ case queue do
+ [head | tail] ->
+ speak(head)
+
+ :timer.sleep(@sleep_between_msg)
+ loop(tail)
+ [] ->
+ receive do
+ msg ->
+ loop(queue ++ [msg])
+ end
+ end
+ end
+
+ defp speak(msg) do
+ System.cmd("espeak", [msg])
+ end
+ end
+#+END_SRC
+
+#+BEGIN_SRC elixir
+ defmodule KVServer do
+ require Logger
+ @max_len_msg 32
+
+ def start(speak_server_pid, port) do
+ {:ok, socket} =
+ :gen_tcp.listen(port, [:binary, packet: :line, active: false, reuseaddr: true])
+
+ loop_acceptor(socket, speak_server_pid)
+ end
+
+ defp loop_acceptor(socket, speak_server_pid) do
+ {:ok, client} = :gen_tcp.accept(socket)
+ Task.start_link(fn -> serve(client, speak_server_pid) end)
+
+ loop_acceptor(socket, speak_server_pid)
+ end
+
+ defp serve(socket, speak_server_pid) do
+ msg = socket
+ |> read_line()
+ |> String.trim()
+
+ if valid_msg(msg) do
+ send(speak_server_pid, msg)
+ end
+
+ serve(socket, speak_server_pid)
+ end
+
+ defp read_line(socket) do
+ {:ok, data} = :gen_tcp.recv(socket, 0)
+ data
+ end
+
+ defp valid_msg(msg), do: String.length(msg) < @max_len_msg && String.match?(msg, ~r/^[A-Za-z ]+$/)
+ end
+
+ android = :"android@a02364151-23.bluezone.usu.edu"
+
+ Node.connect(android)
+ speak_server_pid = Node.spawn(android, &SpeakServer.loop/0)
+
+ KVServer.start(speak_server_pid, 42069)
+#+END_SRC
+
+This demo shows how we can:
++ Connect nodes running Elixir
++ Spawn processes on nodes and inter process communication
++ Basic Elixir constructs (pattern matching, atoms, function calls, referencing functions)
+
+* CheSSH
+With a brief quick exploration into concurrency with Elixir, we can now explore the architecture of CheSSH,
+and the hardware cluster it runs on:
+
+[[./pis.jpeg]]
+
+** Erlang SSH Module - (maybe) building a tic tac toe game!
+So much networking stuff is built on top of Erlang that its standard library - OTP - has implementations for tons of stuff you'd regularly reach for a library to help; ssh, snmp,
+ftp, are all built in "OTP Applications".
+
+It requires a little bit of time with headaches, but the docs are generally pretty good (with occasional source code browsing): [[https://www.erlang.org/doc/man/ssh.html]]
+
+** Architecture
+[[./architecture.png]]
+
+** Lessons Learned
+1. Use Kubernetes (~buildscripts~ is so horribly janky it's actually funny)
+2. Docker was a great idea
+3. Don't hardcode IP's
+4. Don't try to use Multicast
+5. Load balancing SSH
diff --git a/presentation/pis.jpeg b/presentation/pis.jpeg
new file mode 100644
index 0000000..186054b
--- /dev/null
+++ b/presentation/pis.jpeg
Binary files differ
diff --git a/presentation/tic_tac_toe.exs b/presentation/tic_tac_toe.exs
new file mode 100644
index 0000000..e05bed4
--- /dev/null
+++ b/presentation/tic_tac_toe.exs
@@ -0,0 +1,355 @@
+defmodule Generator do
+ def gen_reference() do
+ min = String.to_integer("100000", 36)
+ max = String.to_integer("ZZZZZZ", 36)
+
+ max
+ |> Kernel.-(min)
+ |> :rand.uniform()
+ |> Kernel.+(min)
+ |> Integer.to_string(36)
+ end
+end
+
+defmodule TicTacToe.GameManager do
+ use GenServer
+
+ defmodule State do
+ defstruct games: %{},
+ joinable_games: [],
+ player_games: %{}
+ end
+
+ def start_link(_) do
+ GenServer.start_link(__MODULE__, %{
+ pid: nil
+ })
+ end
+
+ def init(_) do
+ {:ok, %State{}}
+ end
+
+ defp create_board(), do: Enum.map(0..2, fn _ -> Enum.map(0..2, fn _ -> :empty end) end)
+
+ defp create_game(game_id, player) do
+ %{
+ x: player,
+ o: nil,
+ board: create_board()
+ }
+ end
+
+ def handle_info(
+ {:join, %{client_pid: client_pid, username: username, player_id: connection_id} = player},
+ %State{player_games: player_games, games: games, joinable_games: joinable_games} = state
+ ) do
+ if length(joinable_games) == 0 do
+ game_id = Generator.gen_reference()
+ send(client_pid, {:join_game, game_id})
+
+ {:ok,
+ %State{
+ state
+ | games: Map.put(games, game_id, create_game(game_id, player)),
+ joinable_games: joinable_games ++ [game_id],
+ player_games: Map.put(player_games, player_id, game_id)
+ }}
+ else
+ [joining_game_id | rest] = joinable_games
+ game = Map.get(games, joining_game_id)
+ send(game.x.client_pid, :player_joined)
+ send(client_pid, {:join_game, game_id})
+
+ {:ok,
+ %State{
+ state
+ | games: Map.put(games, game_id, %{game | o: player}),
+ joinable_games: rest,
+ connection_games: Map.put(player_games, connection_id, game_id)
+ }}
+ end
+ end
+end
+
+defmodule TicTacToe.SSHDaemon do
+ @port 4000
+ @key_dir "/tmp/keys"
+ use GenServer
+ require Logger
+
+ def start_link(_) do
+ GenServer.start_link(__MODULE__, %{
+ pid: nil
+ })
+ end
+
+ def init(state) do
+ send(self(), :start)
+
+ {:ok, state}
+ end
+
+ def handle_info(:start, state) do
+ game_manager_pid =
+ case GenServer.start_link(TicTacToe.GameManager, [%{}]) do
+ {:ok, game_manager_pid} ->
+ game_manager_pid
+
+ _ ->
+ nil
+ end
+
+ case :ssh.daemon(
+ @port,
+ system_dir: @key_dir,
+ ssh_cli:
+ {TicTacToe.SSHListener,
+ [
+ %TicTacToe.SSHListener.State{
+ game_manager_pid: game_manager_pid
+ }
+ ]},
+ disconnectfun: &on_disconnect/1,
+ id_string: :random,
+ parallel_login: true,
+ max_sessions: 1_000,
+ subsystems: [],
+ no_auth_needed: true
+ ) do
+ {:ok, pid} ->
+ Logger.info("SSH server started on port #{port}, on #{inspect(pid)}")
+
+ Process.link(pid)
+
+ {:noreply, %{state | pid: pid, game_manager_pid: game_manager_pid}, :hibernate}
+
+ {:error, err} ->
+ raise inspect(err)
+ end
+
+ {:noreply, state}
+ end
+
+ def handle_info(_, state), do: {:noreply, state}
+
+ defp on_disconnect(_reason) do
+ Logger.info("#{inspect(self())} disconnected")
+ end
+end
+
+defmodule TicTacToe.SSHListener do
+ alias Chessh.SSH.Client
+
+ alias IO.ANSI
+
+ require Logger
+
+ @behaviour :ssh_server_channel
+ @session_closed_message [
+ ANSI.clear(),
+ ["This session has been closed"]
+ ]
+
+ defmodule State do
+ defstruct channel_id: nil,
+ client_pid: nil,
+ game_manager_pid: nil,
+ connection_ref: nil
+ end
+
+ def init([%State{} = init_state]) do
+ {:ok, init_state}
+ end
+
+ def handle_msg({:ssh_channel_up, channel_id, connection_ref}, %State{} = state) do
+ Logger.debug("SSH channel up #{inspect(:ssh.connection_info(connection_ref))}")
+
+ username =
+ :ssh.connection_info(connection_ref)
+ |> Keyword.fetch!(:user)
+ |> String.Chars.to_string()
+
+ {:ok,
+ %State{
+ state
+ | channel_id: channel_id,
+ connection_ref: connection_ref,
+ player: %{
+ id: Generator.gen_reference(),
+ username: username
+ }
+ }}
+ end
+
+ def handle_msg(
+ {:EXIT, client_pid, _reason},
+ %State{client_pid: client_pid, channel_id: channel_id} = state
+ ) do
+ send(client_pid, :quit)
+ {:stop, channel_id, state}
+ end
+
+ def handle_msg(
+ {:send_data, data},
+ %State{connection_ref: connection_ref, channel_id: channel_id} = state
+ ) do
+ :ssh_connection.send(connection_ref, channel_id, data)
+ {:ok, state}
+ end
+
+ def handle_msg(
+ :session_closed,
+ %State{connection_ref: connection_ref, channel_id: channel_id} = state
+ ) do
+ :ssh_connection.send(connection_ref, channel_id, @session_closed_message)
+ {:stop, channel_id, state}
+ end
+
+ def handle_msg(msg, term) do
+ Logger.debug("Unknown msg #{inspect(msg)}, #{inspect(term)}")
+ end
+
+ def handle_ssh_msg(
+ {:ssh_cm, _connection_handler, {:data, _channel_id, _type, data}},
+ %State{client_pid: client_pid} = state
+ ) do
+ send(client_pid, {:data, data})
+ {:ok, state}
+ end
+
+ def handle_ssh_msg(
+ {:ssh_cm, connection_handler,
+ {:pty, channel_id, want_reply?, {_term, _width, _height, _pixwidth, _pixheight, _opts}}},
+ %State{} = state
+ ) do
+ Logger.debug("#{inspect(state.player_session)} has requested a PTY")
+ :ssh_connection.reply_request(connection_handler, want_reply?, :success, channel_id)
+ {:ok, state}
+ end
+
+ def handle_ssh_msg(
+ {:ssh_cm, connection_handler, {:env, channel_id, want_reply?, var, value}},
+ state
+ ) do
+ :ssh_connection.reply_request(connection_handler, want_reply?, :failure, channel_id)
+
+ {:ok, state}
+ end
+
+ def handle_ssh_msg(
+ {:ssh_cm, _connection_handler,
+ {:window_change, _channel_id, _width, _height, _pixwidth, _pixheight}},
+ %State{client_pid: client_pid} = state
+ ) do
+ {:ok, state}
+ end
+
+ def handle_ssh_msg(
+ {:ssh_cm, connection_handler, {:shell, channel_id, want_reply?}},
+ %State{player: player} = state
+ ) do
+ :ssh_connection.reply_request(connection_handler, want_reply?, :success, channel_id)
+
+ {:ok, client_pid} =
+ GenServer.start_link(Client, [
+ %Client.State{
+ tui_pid: self(),
+ player: player
+ }
+ ])
+
+ send(client_pid, :refresh)
+ {:ok, %State{state | client_pid: client_pid}}
+ end
+
+ def handle_ssh_msg(
+ msg,
+ %State{channel_id: channel_id} = state
+ ) do
+ Logger.debug("UNKOWN MESSAGE #{inspect(msg)}")
+ # {:stop, channel_id, state}
+ {:ok, state}
+ end
+
+ def terminate(_reason, _state) do
+ :ok
+ end
+end
+
+defmodule TicTacToe.Client do
+ alias IO.ANSI
+ use GenServer
+
+ @clear_codes [
+ ANSI.clear(),
+ ANSI.home()
+ ]
+
+ defmodule State do
+ defstruct tui_pid: nil,
+ game_manager_pid: nil,
+ player: %{},
+ game_id: nil
+ end
+
+ @impl true
+ def init([%State{game_manager_pid: game_manager_pid, player: player} = state]) do
+ player = %{
+ player
+ | client_pid: self()
+ }
+
+ send(game_manager_pid, {:join, player})
+
+ {:ok,
+ %State{
+ player: player
+ }}
+ end
+
+ @impl true
+ def handle_info(:quit, %State{} = state) do
+ {:stop, :normal, state}
+ end
+
+ @impl true
+ def handle_info({:join_game, game_id}, %State{} = state) do
+ state = %State{state | game_id: game_id}
+ render(state)
+ {:stop, :normal, state}
+ end
+
+ def handle(
+ {:data, data},
+ %State{} = state
+ ) do
+ case keymap(data) do
+ :quit ->
+ {:stop, :normal, state}
+ end
+ end
+
+ def handle(
+ :player_joined,
+ %State{} = state
+ ) do
+ render(state)
+ {:noreply, state}
+ end
+
+ defp render(%State{
+ tui_pid: tui_pid
+ }) do
+ send(tui_pid, {:send_data, ["Testing"]})
+ end
+
+ def keymap(key) do
+ case key do
+ # Exit keys - C-c and C-d
+ <<3>> -> :quit
+ <<4>> -> :quit
+ x -> x
+ end
+ end
+end