commit 8bd06a67ed6367bdf205de1ce8d716e0b8fd1e74 Author: Magnus von Wachenfeldt Date: Thu Feb 8 22:42:39 2024 +0100 echo server diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84cdb3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# 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"). +protohackers-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d8174ed --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Protohackers + +Implementations of protocols from https://protohackers.com/ + diff --git a/lib/protohackers.ex b/lib/protohackers.ex new file mode 100644 index 0000000..8b880bf --- /dev/null +++ b/lib/protohackers.ex @@ -0,0 +1,18 @@ +defmodule Protohackers do + @moduledoc """ + Documentation for `Protohackers`. + """ + + @doc """ + Hello world. + + ## Examples + + iex> Protohackers.hello() + :world + + """ + def hello do + :world + end +end diff --git a/lib/protohackers/application.ex b/lib/protohackers/application.ex new file mode 100644 index 0000000..8dbb770 --- /dev/null +++ b/lib/protohackers/application.ex @@ -0,0 +1,20 @@ +defmodule Protohackers.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + @impl true + def start(_type, _args) do + children = [ + {Protohackers.EchoServer, port: 5001}, + {Protohackers.PrimeTimeServer, port: 5002} + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: Protohackers.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/lib/protohackers/echo_server.ex b/lib/protohackers/echo_server.ex new file mode 100644 index 0000000..66b051f --- /dev/null +++ b/lib/protohackers/echo_server.ex @@ -0,0 +1,69 @@ +defmodule Protohackers.EchoServer do + use GenServer + + require Logger + + @spec start_link(keyword()) :: GenServer.on_start() + def start_link(opts) do + GenServer.start_link(__MODULE__, opts) + end + + defstruct [:listen_socket, :supervisor] + + @impl true + def init(opts) do + port = Keyword.fetch!(opts, :port) + {:ok, supervisor} = Task.Supervisor.start_link(max_children: 100) + + listen_options = [ + ifaddr: {0, 0, 0, 0}, + mode: :binary, + active: false, + reuseaddr: true, + exit_on_close: false + ] + + case :gen_tcp.listen(port, listen_options) do + {:ok, listen_socket} -> + Logger.info("Started echo server on port #{port}") + state = %__MODULE__{listen_socket: listen_socket, supervisor: supervisor} + {:ok, state, {:continue, :accept}} + + {:error, reason} -> + {:stop, reason} + end + end + + @impl true + def handle_continue(:accept, %__MODULE__{} = state) do + case :gen_tcp.accept(state.listen_socket) do + {:ok, socket} -> + Task.Supervisor.start_child(state.supervisor, fn -> handle_connection(socket) end) + {:noreply, state, {:continue, :accept}} + + {:error, reason} -> + {:stop, reason} + end + end + + ## Helpers + + defp handle_connection(socket) do + case recv_until_closed(socket, _buffer = "", _buffered_size = 0) do + {:ok, data} -> :gen_tcp.send(socket, data) + {:error, reason} -> Logger.error("Failed to receive data: #{inspect(reason)}") + end + + :gen_tcp.close(socket) + end + + @limit _100_kb = 1024 * 100 + defp recv_until_closed(socket, buffer, buffered_size) do + case :gen_tcp.recv(socket, 0, 10_000) do + {:ok, data} when buffered_size + byte_size(data) > @limit -> {:error, :buffer_overflow} + {:ok, data} -> recv_until_closed(socket, [buffer, data], buffered_size + byte_size(data)) + {:error, :closed} -> {:ok, buffer} + {:error, reason} -> {:error, reason} + end + end +end diff --git a/lib/protohackers/prime_time_server.ex b/lib/protohackers/prime_time_server.ex new file mode 100644 index 0000000..59c3560 --- /dev/null +++ b/lib/protohackers/prime_time_server.ex @@ -0,0 +1,69 @@ +defmodule Protohackers.PrimeTimeServer do + use GenServer + + require Logger + + @spec start_link(keyword()) :: GenServer.on_start() + def start_link(opts) do + GenServer.start_link(__MODULE__, opts) + end + + defstruct [:listen_socket, :supervisor] + + @impl true + def init(opts) do + port = Keyword.fetch!(opts, :port) + {:ok, supervisor} = Task.Supervisor.start_link(max_children: 100) + + listen_options = [ + ifaddr: {0, 0, 0, 0}, + mode: :binary, + active: false, + reuseaddr: true, + exit_on_close: false + ] + + case :gen_tcp.listen(port, listen_options) do + {:ok, listen_socket} -> + Logger.info("Started prime time server on port #{port}") + state = %__MODULE__{listen_socket: listen_socket, supervisor: supervisor} + {:ok, state, {:continue, :accept}} + + {:error, reason} -> + {:stop, reason} + end + end + + @impl true + def handle_continue(:accept, %__MODULE__{} = state) do + case :gen_tcp.accept(state.listen_socket) do + {:ok, socket} -> + Task.Supervisor.start_child(state.supervisor, fn -> handle_connection(socket) end) + {:noreply, state, {:continue, :accept}} + + {:error, reason} -> + {:stop, reason} + end + end + + ## Helpers + + defp handle_connection(socket) do + case recv_until_closed(socket, _buffer = "", _buffered_size = 0) do + {:ok, data} -> :gen_tcp.send(socket, data) + {:error, reason} -> Logger.error("Failed to receive data: #{inspect(reason)}") + end + + :gen_tcp.close(socket) + end + + @limit _100_kb = 1024 * 100 + defp recv_until_closed(socket, buffer, buffered_size) do + case :gen_tcp.recv(socket, 0, 10_000) do + {:ok, data} when buffered_size + byte_size(data) > @limit -> {:error, :buffer_overflow} + {:ok, data} -> recv_until_closed(socket, [buffer, data], buffered_size + byte_size(data)) + {:error, :closed} -> {:ok, buffer} + {:error, reason} -> {:error, reason} + end + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..7e99864 --- /dev/null +++ b/mix.exs @@ -0,0 +1,29 @@ +defmodule Protohackers.MixProject do + use Mix.Project + + def project do + [ + app: :protohackers, + version: "0.1.0", + elixir: "~> 1.16", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger], + mod: {Protohackers.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"} + ] + end +end diff --git a/test/protohackers_test.exs b/test/protohackers_test.exs new file mode 100644 index 0000000..496a457 --- /dev/null +++ b/test/protohackers_test.exs @@ -0,0 +1,8 @@ +defmodule ProtohackersTest do + use ExUnit.Case + doctest Protohackers + + test "greets the world" do + assert Protohackers.hello() == :world + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()