├── .formatter.exs
├── .gitignore
├── Dockerfile
├── README.md
├── apps
├── isl
│ ├── .formatter.exs
│ ├── .gitignore
│ ├── lib
│ │ └── isl
│ │ │ ├── application.ex
│ │ │ ├── cipher.ex
│ │ │ └── connection.ex
│ ├── mix.exs
│ ├── mix.lock
│ ├── rel
│ │ ├── env.bat.eex
│ │ ├── env.sh.eex
│ │ ├── remote.vm.args.eex
│ │ └── vm.args.eex
│ └── test
│ │ ├── isl
│ │ └── cipher_test.exs
│ │ ├── isl_test.exs
│ │ └── test_helper.exs
├── line_reversal
│ ├── .formatter.exs
│ ├── .gitignore
│ ├── lib
│ │ └── line_reversal
│ │ │ ├── acceptor.ex
│ │ │ ├── application.ex
│ │ │ ├── connection.ex
│ │ │ ├── lrcp.ex
│ │ │ └── lrcp
│ │ │ ├── listen_socket.ex
│ │ │ ├── protocol.ex
│ │ │ └── socket.ex
│ ├── mix.exs
│ ├── mix.lock
│ ├── rel
│ │ ├── env.bat.eex
│ │ ├── env.sh.eex
│ │ ├── remote.vm.args.eex
│ │ └── vm.args.eex
│ └── test
│ │ ├── line_reversal
│ │ ├── lrcp
│ │ │ └── protocol_test.exs
│ │ └── udp_server_test.exs
│ │ └── test_helper.exs
├── protohackers_first_days
│ ├── lib
│ │ └── protohackers
│ │ │ ├── application.ex
│ │ │ ├── budget_chat_server.ex
│ │ │ ├── echo_server.ex
│ │ │ ├── mitm
│ │ │ ├── acceptor.ex
│ │ │ ├── boguscoin.ex
│ │ │ ├── connection.ex
│ │ │ ├── connection_supervisor.ex
│ │ │ └── supervisor.ex
│ │ │ ├── prices_server.ex
│ │ │ ├── prices_server
│ │ │ └── db.ex
│ │ │ ├── prime_server.ex
│ │ │ └── udp_server.ex
│ ├── mix.exs
│ ├── mix.lock
│ ├── rel
│ │ ├── env.bat.eex
│ │ ├── env.sh.eex
│ │ ├── remote.vm.args.eex
│ │ └── vm.args.eex
│ └── test
│ │ ├── protohackers
│ │ ├── budget_chat_server_test.exs
│ │ ├── echo_server_test.exs
│ │ ├── mitm
│ │ │ └── boguscoin_test.exs
│ │ ├── prices_server
│ │ │ └── db_test.exs
│ │ ├── prices_server_test.exs
│ │ ├── prime_server_test.exs
│ │ └── udp_server_test.exs
│ │ └── test_helper.exs
└── speed_daemon
│ ├── .formatter.exs
│ ├── .gitignore
│ ├── lib
│ └── speed_daemon
│ │ ├── acceptor.ex
│ │ ├── application.ex
│ │ ├── central_ticket_dispatcher.ex
│ │ ├── connection.ex
│ │ ├── connection_supervisor.ex
│ │ ├── message.ex
│ │ └── supervisor.ex
│ ├── mix.exs
│ ├── mix.lock
│ ├── rel
│ ├── env.bat.eex
│ ├── env.sh.eex
│ ├── remote.vm.args.eex
│ └── vm.args.eex
│ └── test
│ ├── speed_daemon
│ ├── integration_test.exs
│ └── message_test.exs
│ └── test_helper.exs
├── config
├── config.exs
└── runtime.exs
├── fly.toml
├── mix.exs
└── mix.lock
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 | ]
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 | /apps/*/_build
4 |
5 | # If you run "mix test --cover", coverage assets end up here.
6 | /cover/
7 |
8 | # The directory Mix downloads your dependencies sources to.
9 | /deps/
10 | /apps/*/deps
11 |
12 | # Where third-party dependencies like ExDoc output generated docs.
13 | /doc/
14 |
15 | # Ignore .fetch files in case you like to edit your project deps locally.
16 | /.fetch
17 |
18 | # If the VM crashes, it generates a dump, let's ignore it too.
19 | erl_crash.dump
20 |
21 | # Also ignore archive artifacts (built via "mix archive.build").
22 | *.ez
23 |
24 | # Ignore package tarball (built via "mix hex.build").
25 | protohackers-*.tar
26 |
27 | # Temporary files, for example, from tests.
28 | /tmp/
29 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG BUILDER_IMAGE="hexpm/elixir:1.14.2-erlang-25.0.4-debian-bullseye-20220801-slim"
2 | ARG RUNNER_IMAGE="debian:bullseye-20220801-slim"
3 |
4 | FROM ${BUILDER_IMAGE} AS builder
5 |
6 | ARG APPLICATION="isl"
7 |
8 | # Set env variables
9 | ENV MIX_ENV="prod"
10 |
11 | # Install build dependencies
12 | RUN apt-get update -y && apt-get install -y build-essential git \
13 | && apt-get clean && rm -f /var/lib/apt/lists/*_*
14 |
15 | WORKDIR /app
16 |
17 | # Install Hex and rebar3
18 | RUN mix do local.hex --force, local.rebar --force
19 |
20 | # Copy configuration from this app and all children
21 | COPY config config
22 |
23 | # Copy mix.exs and mix.lock from all children applications
24 | COPY mix.exs ./
25 | COPY apps/${APPLICATION}/mix.exs apps/${APPLICATION}/mix.exs
26 | COPY apps/${APPLICATION}/mix.lock apps/${APPLICATION}/mix.lock
27 | RUN mix do deps.get --only $MIX_ENV, deps.compile
28 |
29 | # Copy lib for all applications and compile
30 | COPY apps/${APPLICATION}/lib apps/${APPLICATION}/lib
31 | RUN mix compile
32 |
33 | # Changes to config/runtime.exs don't require recompiling the code
34 | COPY apps/${APPLICATION}/rel apps/${APPLICATION}/rel
35 | RUN mix release ${APPLICATION}
36 |
37 | ## Runner image
38 |
39 | FROM ${RUNNER_IMAGE}
40 |
41 | ENV APPLICATION="isl"
42 |
43 | # Install dependencies
44 | RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales \
45 | && apt-get clean && rm -f /var/lib/apt/lists/*_*
46 |
47 | # Set the locale
48 | RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
49 | ENV LANG en_US.UTF-8
50 | ENV LANGUAGE en_US:en
51 | ENV LC_ALL en_US.UTF-8
52 |
53 | WORKDIR /app
54 |
55 | COPY --from=builder /app/_build/prod/rel ./
56 |
57 | CMD /app/${APPLICATION}/bin/${APPLICATION} start
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Protohackers in Elixir
2 |
3 | This repository contains the code for the video series I'm making, focused on
4 | solving the [Protohackers] network challenges in Elixir.
5 |
6 | You can find the video series on [my YouTube channel][youtube-channel], starting
7 | from the first video:
8 |
9 |
10 |
11 |
12 |
13 | ## Running Locally
14 |
15 | You can run this application locally by cloning the repository and running:
16 |
17 | ```shell
18 | mix deps.get
19 | mix run --no-halt
20 | ```
21 |
22 | ## Deploying
23 |
24 | I'm deploying this application on [Fly.io][fly].
25 |
26 | [Protohackers]: https://protohackers.com
27 | [youtube-channel]: https://www.youtube.com/channel/UCiaFBwlunX1m8FKwZQ1GOSA
28 | [fly]: https://fly.io
29 |
--------------------------------------------------------------------------------
/apps/isl/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 | ]
5 |
--------------------------------------------------------------------------------
/apps/isl/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where third-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | isl-*.tar
24 |
25 | # Temporary files, for example, from tests.
26 | /tmp/
27 |
--------------------------------------------------------------------------------
/apps/isl/lib/isl/application.ex:
--------------------------------------------------------------------------------
1 | defmodule ISL.Application do
2 | # See https://hexdocs.pm/elixir/Application.html
3 | # for more information on OTP Applications
4 | @moduledoc false
5 |
6 | use Application
7 |
8 | @impl true
9 | def start(_type, _args) do
10 | port = String.to_integer(System.get_env("TCP_PORT", "5009"))
11 |
12 | children = [
13 | {ThousandIsland, port: port, handler_module: ISL.Connection}
14 | ]
15 |
16 | # See https://hexdocs.pm/elixir/Supervisor.html
17 | # for other strategies and supported options
18 | opts = [strategy: :one_for_one, name: Isl.Supervisor]
19 | Supervisor.start_link(children, opts)
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/apps/isl/lib/isl/cipher.ex:
--------------------------------------------------------------------------------
1 | defmodule ISL.Cipher do
2 | @type spec() :: [
3 | :reversebits
4 | | {:xor, byte()}
5 | | :xorpos
6 | | {:add, byte()}
7 | | :addpos
8 | | {:sub, byte()}
9 | | :subpos
10 | ]
11 |
12 | import Bitwise
13 |
14 | require Integer
15 |
16 | @doc """
17 | ## Examples
18 |
19 | iex> ISL.Cipher.parse_spec(<<0x00>>)
20 | {:ok, [], <<>>}
21 |
22 | iex> ISL.Cipher.parse_spec(<<0x00, "hello">>)
23 | {:ok, [], "hello"}
24 |
25 | iex> ISL.Cipher.parse_spec(<<0x02, 0xaa, 0x01, 0x00, "hello">>)
26 | {:ok, [{:xor, 0xaa}, :reversebits], "hello"}
27 |
28 | iex> ISL.Cipher.parse_spec(<<0xff>>)
29 | :error
30 |
31 | iex> ISL.Cipher.parse_spec(<<0x01>>)
32 | :error
33 |
34 | """
35 | @spec parse_spec(binary()) :: {:ok, spec(), binary()} | :error
36 | def parse_spec(binary) when is_binary(binary) do
37 | parse_spec(binary, _acc = [])
38 | end
39 |
40 | defp parse_spec(<<0x00, rest::binary>>, acc), do: {:ok, Enum.reverse(acc), rest}
41 | defp parse_spec(<<0x01, rest::binary>>, acc), do: parse_spec(rest, [:reversebits | acc])
42 | defp parse_spec(<<0x02, n, rest::binary>>, acc), do: parse_spec(rest, [{:xor, n} | acc])
43 | defp parse_spec(<<0x03, rest::binary>>, acc), do: parse_spec(rest, [:xorpos | acc])
44 | defp parse_spec(<<0x04, n, rest::binary>>, acc), do: parse_spec(rest, [{:add, n} | acc])
45 | defp parse_spec(<<0x05, rest::binary>>, acc), do: parse_spec(rest, [:addpos | acc])
46 | defp parse_spec(_other, _acc), do: :error
47 |
48 | @doc """
49 |
50 | iex> ISL.Cipher.apply("hello", [{:xor, 1}, :reversebits], 0)
51 | <<0x96, 0x26, 0xb6, 0xb6, 0x76>>
52 |
53 | """
54 | @spec apply(binary(), spec(), non_neg_integer()) :: binary()
55 | def apply(data, spec, start_position) do
56 | {encoded, _position} =
57 | for <>, reduce: {_acc = <<>>, start_position} do
58 | {acc, position} ->
59 | encoded = Enum.reduce(spec, byte, &apply_operation(&1, &2, position))
60 | {<>, position + 1}
61 | end
62 |
63 | encoded
64 | end
65 |
66 | defp apply_operation(:reversebits, byte, _position), do: reverse_bits(byte)
67 | defp apply_operation({:xor, n}, byte, _position), do: bxor(n, byte)
68 | defp apply_operation(:xorpos, byte, position), do: bxor(byte, position)
69 | defp apply_operation({:add, n}, byte, _position), do: rem(byte + n, 256)
70 | defp apply_operation(:addpos, byte, position), do: rem(byte + position, 256)
71 | defp apply_operation({:sub, n}, byte, _position), do: rem(byte - n, 256)
72 | defp apply_operation(:subpos, byte, position), do: rem(byte - position, 256)
73 |
74 | @doc """
75 |
76 | iex> ISL.Cipher.reverse_bits(0b00000001)
77 | 0b10000000
78 |
79 | """
80 | @spec reverse_bits(byte()) :: byte()
81 | def reverse_bits(byte) do
82 | <> = <>
83 | <> = <>
84 | reversed
85 | end
86 |
87 | @doc """
88 | iex> ISL.Cipher.reverse_spec([:reversebits, :xorpos, :addpos, {:xor, 3}, {:add, 9}])
89 | [{:sub, 9}, {:xor, 3}, :subpos, :xorpos, :reversebits]
90 | """
91 | @spec reverse_spec(spec()) :: spec()
92 | def reverse_spec(spec) do
93 | spec
94 | |> Enum.reverse()
95 | |> Enum.map(fn
96 | :addpos -> :subpos
97 | {:add, n} -> {:sub, n}
98 | other -> other
99 | end)
100 | end
101 |
102 | @spec no_op?(spec()) :: boolean()
103 | def no_op?(ops) do
104 | {xorpos_ops, ops} = Enum.split_with(ops, &(&1 == :xorpos))
105 |
106 | {adds, ops} = Enum.split_with(ops, &match?({:add, _}, &1))
107 | total_add = Enum.reduce(adds, 0, fn {:add, n}, acc -> rem(n + acc, 256) end)
108 | {addpos_ops, ops} = Enum.split_with(ops, &(&1 == :addpos))
109 |
110 | cond do
111 | Integer.is_odd(length(xorpos_ops)) -> false
112 | addpos_ops != [] -> false
113 | total_add != 0 -> false
114 | true -> no_op_rest?(ops, _xor_acc = 0, _reversed? = false)
115 | end
116 | end
117 |
118 | defp no_op_rest?([], _xor_acc = 0, _reversed? = false), do: true
119 | defp no_op_rest?([], _xor_acc, _reversed?), do: false
120 |
121 | defp no_op_rest?([:reversebits, :reversebits | rest], xor_acc, reversed?) do
122 | no_op_rest?(rest, xor_acc, reversed?)
123 | end
124 |
125 | defp no_op_rest?([:reversebits | [{:xor, _} | _] = rest], xor_acc, reversed?) do
126 | rest
127 | |> Enum.map(fn
128 | {:xor, n} -> {:xor, reverse_bits(n)}
129 | other -> other
130 | end)
131 | |> no_op_rest?(xor_acc, not reversed?)
132 | end
133 |
134 | defp no_op_rest?([{:xor, _} | _] = spec, xor_acc, reversed?) do
135 | {xors, rest} = Enum.split_while(spec, &match?({:xor, _}, &1))
136 | xor_acc = Enum.reduce(xors, xor_acc, fn {:xor, n}, acc -> bxor(n, acc) end)
137 | no_op_rest?(rest, xor_acc, reversed?)
138 | end
139 |
140 | defp no_op_rest?([:reversebits | rest], xor_acc, reversed?) do
141 | no_op_rest?(rest, xor_acc, not reversed?)
142 | end
143 | end
144 |
--------------------------------------------------------------------------------
/apps/isl/lib/isl/connection.ex:
--------------------------------------------------------------------------------
1 | defmodule ISL.Connection do
2 | use ThousandIsland.Handler
3 |
4 | alias ISL.Cipher
5 |
6 | require Logger
7 |
8 | defstruct [
9 | :cipher_spec,
10 | :reverse_cipher_spec,
11 | buffer: <<>>,
12 | client_position: 0,
13 | server_position: 0
14 | ]
15 |
16 | @impl true
17 | def handle_connection(_socket, _handler_opts = []) do
18 | {:continue, %__MODULE__{}}
19 | end
20 |
21 | @impl true
22 | def handle_data(data, socket, state) do
23 | Logger.debug("<-- #{inspect(data, base: :hex)}")
24 |
25 | case handle_new_data(state, socket, data) do
26 | {:ok, state} -> {:continue, state}
27 | :error -> {:close, state}
28 | end
29 | end
30 |
31 | defp handle_new_data(%__MODULE__{cipher_spec: nil} = state, socket, data) do
32 | state = update_in(state.buffer, &(&1 <> data))
33 |
34 | case parse_cipher_spec(state) do
35 | {:ok, state, rest} -> handle_new_data(state, socket, rest)
36 | :error -> :error
37 | end
38 | end
39 |
40 | defp handle_new_data(%__MODULE__{} = state, socket, data) do
41 | data = Cipher.apply(data, state.reverse_cipher_spec, state.client_position)
42 | Logger.debug("Handling decoded data at position #{state.client_position}: #{inspect(data)}")
43 | state = update_in(state.client_position, &(&1 + byte_size(data)))
44 | state = update_in(state.buffer, &(&1 <> data))
45 | handle_new_decoded_data(state, socket)
46 | end
47 |
48 | defp parse_cipher_spec(%__MODULE__{buffer: data, cipher_spec: nil} = state) do
49 | case Cipher.parse_spec(data) do
50 | {:ok, cipher_spec, rest} ->
51 | if Cipher.no_op?(cipher_spec) do
52 | Logger.error("No-op cipher spec")
53 | :error
54 | else
55 | Logger.debug("Parsed cipher: #{inspect(cipher_spec)}")
56 | state = put_in(state.cipher_spec, cipher_spec)
57 | state = put_in(state.reverse_cipher_spec, Cipher.reverse_spec(cipher_spec))
58 | state = put_in(state.buffer, <<>>)
59 | {:ok, state, rest}
60 | end
61 |
62 | :error ->
63 | :error
64 | end
65 | end
66 |
67 | defp handle_new_decoded_data(%__MODULE__{} = state, socket) do
68 | case String.split(state.buffer, "\n", parts: 2) do
69 | [line, rest] ->
70 | state = put_in(state.buffer, rest)
71 | state = handle_line(state, socket, line)
72 | handle_new_decoded_data(state, socket)
73 |
74 | [_buffer] ->
75 | {:ok, state}
76 | end
77 | end
78 |
79 | defp handle_line(%__MODULE__{} = state, socket, line) do
80 | encoded =
81 | line
82 | |> String.split(",")
83 | |> Enum.max_by(fn toy_spec ->
84 | case Integer.parse(toy_spec) do
85 | {quantity, "x " <> _toy} -> quantity
86 | :error -> raise "invalid packet"
87 | end
88 | end)
89 | |> Kernel.<>("\n")
90 | |> Cipher.apply(state.cipher_spec, state.server_position)
91 |
92 | ThousandIsland.Socket.send(socket, encoded)
93 | update_in(state.server_position, &(&1 + byte_size(encoded)))
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/apps/isl/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule ISL.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :isl,
7 | version: "0.1.0",
8 | build_path: "../../_build",
9 | config_path: "../../config/config.exs",
10 | deps_path: "../../deps",
11 | lockfile: "../../mix.lock",
12 | elixir: "~> 1.14",
13 | start_permanent: Mix.env() == :prod,
14 | deps: deps()
15 | ]
16 | end
17 |
18 | # Run "mix help compile.app" to learn about applications.
19 | def application do
20 | [
21 | extra_applications: [:logger],
22 | mod: {ISL.Application, []}
23 | ]
24 | end
25 |
26 | # Run "mix help deps" to learn about dependencies.
27 | defp deps do
28 | [
29 | {:thousand_island, "~> 0.6.0"}
30 | ]
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/apps/isl/mix.lock:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whatyouhide/protohackers_in_elixir/f73015610479467c0d60fcaeda6da8dbcc4d6538/apps/isl/mix.lock
--------------------------------------------------------------------------------
/apps/isl/rel/env.bat.eex:
--------------------------------------------------------------------------------
1 | @echo off
2 | rem Set the release to load code on demand (interactive) instead of preloading (embedded).
3 | rem set RELEASE_MODE=interactive
4 |
5 | rem Set the release to work across nodes.
6 | rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
7 | rem set RELEASE_DISTRIBUTION=name
8 | rem set RELEASE_NODE=<%= @release.name %>
9 |
--------------------------------------------------------------------------------
/apps/isl/rel/env.sh.eex:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ip=$(grep fly-local-6pn /etc/hosts | cut -f 1)
4 | export RELEASE_DISTRIBUTION=name
5 | export RELEASE_NODE=$FLY_APP_NAME@"$ip"
6 | export ELIXIR_ERL_OPTIONS="-proto_dist inet6_tcp"
7 |
--------------------------------------------------------------------------------
/apps/isl/rel/remote.vm.args.eex:
--------------------------------------------------------------------------------
1 | ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html
2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here
3 |
4 | ## Increase number of concurrent ports/sockets
5 | ##+Q 65536
6 |
7 | ## Tweak GC to run more often
8 | ##-env ERL_FULLSWEEP_AFTER 10
9 |
--------------------------------------------------------------------------------
/apps/isl/rel/vm.args.eex:
--------------------------------------------------------------------------------
1 | ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html
2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here
3 |
4 | ## Increase number of concurrent ports/sockets
5 | ##+Q 65536
6 |
7 | ## Tweak GC to run more often
8 | ##-env ERL_FULLSWEEP_AFTER 10
9 |
--------------------------------------------------------------------------------
/apps/isl/test/isl/cipher_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ISL.CipherTest do
2 | use ExUnit.Case, async: true
3 |
4 | doctest ISL.Cipher
5 | end
6 |
--------------------------------------------------------------------------------
/apps/isl/test/isl_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ISLTest do
2 | use ExUnit.Case, async: true
3 |
4 | test "example session from the problem description" do
5 | {:ok, client} = :gen_tcp.connect(~c"localhost", 5009, [:binary, active: true])
6 |
7 | :ok = :gen_tcp.send(client, <<0x02, 0x7B, 0x05, 0x01, 0x00>>)
8 |
9 | :ok =
10 | :gen_tcp.send(
11 | client,
12 | <<0xF2, 0x20, 0xBA, 0x44, 0x18, 0x84, 0xBA, 0xAA, 0xD0, 0x26, 0x44, 0xA4, 0xA8, 0x7E>>
13 | )
14 |
15 | assert_receive {:tcp, ^client, <<0x72, 0x20, 0xBA, 0xD8, 0x78, 0x70, 0xEE>>}, 500
16 |
17 | :ok =
18 | :gen_tcp.send(
19 | client,
20 | <<0x6A, 0x48, 0xD6, 0x58, 0x34, 0x44, 0xD6, 0x7A, 0x98, 0x4E, 0x0C, 0xCC, 0x94, 0x31>>
21 | )
22 |
23 | assert_receive {:tcp, ^client, <<0xF2, 0xD0, 0x26, 0xC8, 0xA4, 0xD8, 0x7E>>}, 500
24 | end
25 |
26 | @tag :capture_log
27 | test "no-op ciphers result in the client being disconnected" do
28 | {:ok, client} = :gen_tcp.connect(~c"localhost", 5009, [:binary, active: true])
29 |
30 | # Cipher spec from the problem description
31 | :ok = :gen_tcp.send(client, <<0x02, 0xA0, 0x02, 0x0B, 0x02, 0xAB, 0x00>>)
32 |
33 | assert_receive {:tcp_closed, ^client}
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/apps/isl/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/apps/line_reversal/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 | ]
5 |
--------------------------------------------------------------------------------
/apps/line_reversal/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where third-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | line_reversal-*.tar
24 |
25 | # Temporary files, for example, from tests.
26 | /tmp/
27 |
--------------------------------------------------------------------------------
/apps/line_reversal/lib/line_reversal/acceptor.ex:
--------------------------------------------------------------------------------
1 | defmodule LineReversal.Acceptor do
2 | use Task, restart: :transient
3 |
4 | alias LineReversal.{LRCP, Connection}
5 |
6 | require Logger
7 |
8 | @spec start_link(keyword()) :: {:ok, pid()}
9 | def start_link(options) when is_list(options) do
10 | ip = Keyword.fetch!(options, :ip)
11 | port = Keyword.fetch!(options, :port)
12 | Task.start_link(__MODULE__, :__accept__, [ip, port])
13 | end
14 |
15 | ## Private
16 |
17 | def __accept__(ip, port) when is_tuple(ip) and is_integer(port) do
18 | case LRCP.listen(ip, port) do
19 | {:ok, listen_socket} ->
20 | Logger.info("Listening for LRCP connections on port #{port}")
21 | loop(listen_socket)
22 |
23 | {:error, reason} ->
24 | raise "failed to start LRCP listen socket on port #{port}: #{inspect(reason)}"
25 | end
26 | end
27 |
28 | defp loop(listen_socket) do
29 | case LRCP.accept(listen_socket) do
30 | {:ok, socket} ->
31 | {:ok, handler} = Connection.start_link(socket)
32 | :ok = LRCP.controlling_process(socket, handler)
33 | loop(listen_socket)
34 |
35 | {:error, reason} ->
36 | raise "failed to accept LRCP connection: #{inspect(reason)}"
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/apps/line_reversal/lib/line_reversal/application.ex:
--------------------------------------------------------------------------------
1 | defmodule LineReversal.Application do
2 | @moduledoc false
3 |
4 | use Application
5 |
6 | @impl true
7 | def start(_type, _args) do
8 | children = [
9 | {Registry, name: LineReversal.Registry, keys: :unique},
10 | {LineReversal.Acceptor,
11 | port: String.to_integer(System.get_env("UDP_PORT", "5008")), ip: udp_ip_address()}
12 | ]
13 |
14 | opts = [strategy: :one_for_one, name: LineReversal.Supervisor]
15 | Supervisor.start_link(children, opts)
16 | end
17 |
18 | defp udp_ip_address do
19 | case System.fetch_env("FLY_APP_NAME") do
20 | {:ok, _} ->
21 | {:ok, fly_global_ip} = :inet.getaddr(~c"fly-global-services", :inet)
22 | fly_global_ip
23 |
24 | :error ->
25 | {0, 0, 0, 0}
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/apps/line_reversal/lib/line_reversal/connection.ex:
--------------------------------------------------------------------------------
1 | defmodule LineReversal.Connection do
2 | use GenServer, restart: :temporary
3 |
4 | alias LineReversal.LRCP
5 |
6 | require Logger
7 |
8 | @spec start_link(LRCP.socket()) :: GenServer.on_start()
9 | def start_link(lrcp_socket) do
10 | GenServer.start_link(__MODULE__, lrcp_socket)
11 | end
12 |
13 | ## Callbacks
14 |
15 | defstruct [:socket, buffer: <<>>]
16 |
17 | @impl true
18 | def init(socket) do
19 | Logger.debug("Connection started: #{inspect(socket)}")
20 | {:ok, %__MODULE__{socket: socket}}
21 | end
22 |
23 | @impl true
24 | def handle_info(message, state)
25 |
26 | def handle_info({:lrcp, socket, data}, %__MODULE__{socket: socket} = state) do
27 | Logger.debug("Received LRCP data: #{inspect(data)}")
28 | state = update_in(state.buffer, &(&1 <> data))
29 | state = handle_new_data(state)
30 | {:noreply, state}
31 | end
32 |
33 | def handle_info({:lrcp_error, socket, reason}, %__MODULE__{socket: socket} = state) do
34 | Logger.error("Closing connection due to error: #{inspect(reason)}")
35 | {:stop, :normal, state}
36 | end
37 |
38 | def handle_info({:lrcp_closed, socket}, %__MODULE__{socket: socket} = state) do
39 | Logger.debug("Connection closed")
40 | {:stop, :normal, state}
41 | end
42 |
43 | ## Helpers
44 |
45 | defp handle_new_data(%__MODULE__{} = state) do
46 | case String.split(state.buffer, "\n", parts: 2) do
47 | [line, rest] ->
48 | LRCP.send(state.socket, String.reverse(line) <> "\n")
49 | handle_new_data(put_in(state.buffer, rest))
50 |
51 | [_no_line_yet] ->
52 | state
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/apps/line_reversal/lib/line_reversal/lrcp.ex:
--------------------------------------------------------------------------------
1 | defmodule LineReversal.LRCP do
2 | alias LineReversal.LRCP.{ListenSocket, Socket}
3 |
4 | @type listen_socket() :: ListenSocket.t()
5 | @type socket() :: Socket.t()
6 |
7 | @spec listen(:inet.ip_address(), :inet.port_number()) ::
8 | {:ok, listen_socket()} | {:error, term()}
9 | def listen(ip, port) when is_tuple(ip) and is_integer(port) do
10 | ListenSocket.start_link(ip: ip, port: port)
11 | end
12 |
13 | @spec accept(listen_socket()) :: {:ok, socket()} | {:error, term()}
14 | def accept(%ListenSocket{} = listen_socket) do
15 | ListenSocket.accept(listen_socket)
16 | end
17 |
18 | @spec controlling_process(socket(), pid()) :: :ok | {:error, term()}
19 | def controlling_process(%Socket{} = socket, pid) when is_pid(pid) do
20 | Socket.controlling_process(socket, pid)
21 | end
22 |
23 | @spec send(socket(), binary()) :: :ok | {:error, term()}
24 | def send(%Socket{} = socket, data) when is_binary(data) do
25 | Socket.send(socket, data)
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/apps/line_reversal/lib/line_reversal/lrcp/listen_socket.ex:
--------------------------------------------------------------------------------
1 | defmodule LineReversal.LRCP.ListenSocket do
2 | use GenServer
3 |
4 | alias LineReversal.LRCP
5 |
6 | require Logger
7 |
8 | @type t() :: %__MODULE__{pid: pid()}
9 |
10 | defstruct [:pid]
11 |
12 | @spec start_link(keyword()) :: GenServer.on_start()
13 | def start_link(options) when is_list(options) do
14 | with {:ok, pid} <- GenServer.start_link(__MODULE__, options) do
15 | {:ok, %__MODULE__{pid: pid}}
16 | end
17 | end
18 |
19 | @spec accept(t()) :: {:ok, LRCP.socket()} | {:error, term()}
20 | def accept(%__MODULE__{pid: pid} = _listen_socket) do
21 | GenServer.call(pid, :accept, _timeout = :infinity)
22 | end
23 |
24 | ## Callbacks
25 |
26 | defmodule State do
27 | defstruct [
28 | :udp_socket,
29 | :supervisor,
30 | accept_queue: :queue.new(),
31 | ready_sockets: :queue.new()
32 | ]
33 | end
34 |
35 | @impl true
36 | def init(options) do
37 | ip = Keyword.fetch!(options, :ip)
38 | port = Keyword.fetch!(options, :port)
39 |
40 | udp_options = [
41 | :binary,
42 | active: :once,
43 | recbuf: 10_000,
44 | ip: ip
45 | ]
46 |
47 | Logger.metadata(address: "#{:inet.ntoa(ip)}:#{port}")
48 |
49 | with {:ok, udp_socket} <- :gen_udp.open(port, udp_options),
50 | {:ok, supervisor} <- DynamicSupervisor.start_link(max_children: 200) do
51 | Logger.debug("Listening for UDP connections")
52 | {:ok, %State{udp_socket: udp_socket, supervisor: supervisor}}
53 | else
54 | {:error, reason} -> {:stop, reason}
55 | end
56 | end
57 |
58 | @impl true
59 | def handle_call(:accept, from, state) do
60 | case get_and_update_in(state.ready_sockets, &:queue.out/1) do
61 | # There is a socket ready to be handled.
62 | {{:value, %LRCP.Socket{} = socket}, state} ->
63 | Logger.debug("Accepted connection #{inspect(socket)} from the queue")
64 | {:reply, {:ok, socket}, state}
65 |
66 | # No sockets are ready, so we queue this client for when a socket is ready.
67 | {:empty, state} ->
68 | state = update_in(state.accept_queue, &:queue.in(from, &1))
69 | Logger.debug("Queued accept call")
70 | {:noreply, state}
71 | end
72 | end
73 |
74 | @impl true
75 | def handle_info({:udp, udp_socket, ip, port, packet}, %State{udp_socket: udp_socket} = state) do
76 | :ok = :inet.setopts(udp_socket, active: :once)
77 | Logger.debug("<-- #{inspect(packet)}")
78 |
79 | case LRCP.Protocol.parse_packet(packet) do
80 | {:ok, packet} ->
81 | handle_packet(state, ip, port, packet)
82 |
83 | :error ->
84 | Logger.debug("Invalid packet, ignoring it: #{inspect(packet)}")
85 | {:noreply, state}
86 | end
87 | end
88 |
89 | ## Helpers
90 |
91 | defp handle_packet(state, ip, port, {:connect, session_id}) do
92 | spec = {LRCP.Socket, [%__MODULE__{pid: self()}, state.udp_socket, ip, port, session_id]}
93 |
94 | case DynamicSupervisor.start_child(state.supervisor, spec) do
95 | # We started a new child.
96 | {:ok, socket_pid} ->
97 | socket = %LRCP.Socket{pid: socket_pid}
98 |
99 | case get_and_update_in(state.accept_queue, &:queue.out/1) do
100 | # If there is a pending accept, we can reply to it.
101 | {{:value, from}, state} ->
102 | Logger.debug("Handing over socket #{inspect(socket)} to queued client")
103 | GenServer.reply(from, {:ok, socket})
104 | {:noreply, state}
105 |
106 | # If there is nothing blocked on accepting, we queue this socket.
107 | {:empty, state} ->
108 | state = update_in(state.ready_sockets, &:queue.in(socket, &1))
109 | {:noreply, state}
110 | end
111 |
112 | # The connection for this session ID is already running, so we just resend the ack.
113 | {:error, {:already_started, _pid}} ->
114 | :ok = LRCP.Socket.resend_connect_ack(%__MODULE__{pid: self()}, session_id)
115 | {:noreply, state}
116 |
117 | {:error, reason} ->
118 | Logger.error("Failed to start connection: #{inspect(reason)}")
119 | {:noreply, state}
120 | end
121 | end
122 |
123 | defp handle_packet(state, ip, port, {:close, session_id}) do
124 | _ = LRCP.Socket.close(%__MODULE__{pid: self()}, session_id)
125 | send_close(state, ip, port, session_id)
126 | {:noreply, state}
127 | end
128 |
129 | defp handle_packet(state, ip, port, packet) do
130 | case LRCP.Socket.handle_packet(%__MODULE__{pid: self()}, packet) do
131 | :ok -> :ok
132 | :not_found -> send_close(state, ip, port, LRCP.Protocol.session_id(packet))
133 | end
134 |
135 | {:noreply, state}
136 | end
137 |
138 | defp send_close(state, ip, port, session_id) do
139 | Logger.debug("--> \"/close/#{session_id}/\"")
140 | :ok = :gen_udp.send(state.udp_socket, ip, port, "/close/#{session_id}/")
141 | end
142 | end
143 |
--------------------------------------------------------------------------------
/apps/line_reversal/lib/line_reversal/lrcp/protocol.ex:
--------------------------------------------------------------------------------
1 | defmodule LineReversal.LRCP.Protocol do
2 | @type session_id() :: integer()
3 |
4 | @type packet() ::
5 | {:connect, session_id()}
6 | | {:close, session_id()}
7 | | {:data, session_id(), integer(), binary()}
8 | | {:ack, session_id(), integer()}
9 |
10 | @max_int 2_147_483_648
11 |
12 | @spec session_id(packet()) :: session_id()
13 | def session_id({:connect, session_id}), do: session_id
14 | def session_id({:close, session_id}), do: session_id
15 | def session_id({:data, session_id, _position, _data}), do: session_id
16 | def session_id({:ack, session_id, _position}), do: session_id
17 |
18 | @spec parse_packet(binary()) :: {:ok, packet()} | :error
19 | def parse_packet(binary) do
20 | with </, rest::binary>> <- binary,
21 | {:ok, parts} <- split(rest, _acc = [], _part = <<>>) do
22 | parse_packet_fields(parts)
23 | else
24 | _other -> :error
25 | end
26 | end
27 |
28 | defp split(<<>>, _acc, _part), do: :error
29 | defp split(</>> = _end, acc, part), do: {:ok, Enum.reverse([part | acc])}
30 | defp split(<<"\\/", rest::binary>>, acc, part), do: split(rest, acc, <>)
31 | defp split(</, rest::binary>>, acc, part), do: split(rest, [part | acc], <<>>)
32 | defp split(<>, acc, part), do: split(rest, acc, <>)
33 |
34 | defp parse_packet_fields([type, session_id]) when type in ["connect", "close"] do
35 | with {:ok, session_id} <- parse_int(session_id) do
36 | {:ok, {String.to_existing_atom(type), session_id}}
37 | end
38 | end
39 |
40 | defp parse_packet_fields(["data", session_id, position, data]) do
41 | with {:ok, session_id} <- parse_int(session_id),
42 | {:ok, position} <- parse_int(position) do
43 | {:ok, {:data, session_id, position, data}}
44 | end
45 | end
46 |
47 | defp parse_packet_fields(["ack", session_id, position]) do
48 | with {:ok, session_id} <- parse_int(session_id),
49 | {:ok, position} <- parse_int(position) do
50 | {:ok, {:ack, session_id, position}}
51 | end
52 | end
53 |
54 | defp parse_packet_fields(_other) do
55 | :error
56 | end
57 |
58 | defp parse_int(bin) do
59 | case Integer.parse(bin) do
60 | {int, ""} when int < @max_int -> {:ok, int}
61 | _ -> :error
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/apps/line_reversal/lib/line_reversal/lrcp/socket.ex:
--------------------------------------------------------------------------------
1 | defmodule LineReversal.LRCP.Socket do
2 | use GenServer, restart: :temporary
3 |
4 | import Kernel, except: [send: 2]
5 |
6 | alias LineReversal.LRCP
7 |
8 | require Logger
9 |
10 | @type t() :: %__MODULE__{pid: pid()}
11 |
12 | @max_data_length 1_000 - String.length("/data/2147483648/2147483648//")
13 | @idle_timeout 60_000
14 |
15 | if Mix.env() == :test do
16 | @retransmit_interval 100
17 | else
18 | @retransmit_interval 3_000
19 | end
20 |
21 | defstruct [:pid]
22 |
23 | @spec start_link(list()) :: GenServer.on_start()
24 | def start_link([
25 | %LRCP.ListenSocket{} = listen_socket,
26 | udp_socket,
27 | peer_ip,
28 | peer_port,
29 | session_id
30 | ]) do
31 | name = name(listen_socket, session_id)
32 | GenServer.start_link(__MODULE__, {udp_socket, peer_ip, peer_port, session_id}, name: name)
33 | end
34 |
35 | @spec send(t(), binary()) :: :ok | {:error, term()}
36 | def send(%__MODULE__{} = socket, data) when is_binary(data) do
37 | GenServer.call(socket.pid, {:send, data})
38 | end
39 |
40 | @spec controlling_process(t(), pid()) :: :ok
41 | def controlling_process(%__MODULE__{} = socket, pid) when is_pid(pid) do
42 | GenServer.call(socket.pid, {:controlling_process, pid})
43 | catch
44 | :exit, {:noproc, _} ->
45 | Kernel.send(pid, {:lrcp_closed, socket})
46 | :ok
47 | end
48 |
49 | @spec resend_connect_ack(LRCP.listen_socket(), integer()) :: :ok
50 | def resend_connect_ack(%LRCP.ListenSocket{} = listen_socket, session_id) do
51 | GenServer.cast(name(listen_socket, session_id), :resend_connect_ack)
52 | end
53 |
54 | @spec handle_packet(LRCP.listen_socket(), packet) :: :ok | :not_found
55 | when packet:
56 | {:data, LRCP.Protocol.session_id(), integer(), binary()}
57 | | {:ack, LRCP.Protocol.session_id(), integer()}
58 | def handle_packet(%LRCP.ListenSocket{} = listen_socket, packet) when is_tuple(packet) do
59 | session_id = LRCP.Protocol.session_id(packet)
60 |
61 | if pid = GenServer.whereis(name(listen_socket, session_id)) do
62 | GenServer.cast(pid, {:handle_packet, packet})
63 | else
64 | :not_found
65 | end
66 | end
67 |
68 | @spec close(LRCP.listen_socket(), integer()) :: :ok
69 | def close(%LRCP.ListenSocket{} = listen_socket, session_id) do
70 | GenServer.cast(name(listen_socket, session_id), :close)
71 | end
72 |
73 | defp name(%LRCP.ListenSocket{} = listen_socket, session_id) when is_integer(session_id) do
74 | {:via, Registry, {LineReversal.Registry, {listen_socket, session_id}}}
75 | end
76 |
77 | ## Callbacks
78 |
79 | defmodule State do
80 | defstruct [
81 | :udp_socket,
82 | :peer_ip,
83 | :peer_port,
84 | :session_id,
85 | :controlling_process,
86 | :idle_timer_ref,
87 | in_position: 0,
88 | out_position: 0,
89 | acked_out_position: 0,
90 | pending_out_payload: <<>>,
91 | out_message_queue: :queue.new()
92 | ]
93 | end
94 |
95 | @impl true
96 | def init({udp_socket, peer_ip, peer_port, session_id}) do
97 | Logger.metadata(session: session_id)
98 |
99 | idle_timer_ref = Process.send_after(self(), :idle_timeout, @idle_timeout)
100 |
101 | state = %State{
102 | udp_socket: udp_socket,
103 | peer_ip: peer_ip,
104 | peer_port: peer_port,
105 | session_id: session_id,
106 | idle_timer_ref: idle_timer_ref
107 | }
108 |
109 | udp_send(state, "/ack/#{state.session_id}/0/")
110 |
111 | {:ok, state}
112 | end
113 |
114 | @impl true
115 | def handle_info(message, state)
116 |
117 | def handle_info(:idle_timeout, %State{} = state) do
118 | Logger.info("Closing connection due to inactivity")
119 | {:stop, :normal, state}
120 | end
121 |
122 | def handle_info(:retransmit_pending_data, %State{} = state) do
123 | state = update_in(state.out_position, &(&1 - byte_size(state.pending_out_payload)))
124 | {:noreply, send_data(state, state.pending_out_payload)}
125 | end
126 |
127 | @impl true
128 | def handle_call({:send, data}, _from, %State{} = state) do
129 | state = update_in(state.pending_out_payload, &(&1 <> data))
130 | state = send_data(state, data)
131 | {:reply, :ok, state}
132 | end
133 |
134 | def handle_call({:controlling_process, pid}, _from, %State{} = state) do
135 | Logger.debug("Controlling process set to #{inspect(pid)}")
136 | state = put_in(state.controlling_process, pid)
137 |
138 | {messages, state} =
139 | get_and_update_in(state.out_message_queue, fn queue ->
140 | {:queue.to_list(queue), :queue.new()}
141 | end)
142 |
143 | Enum.each(messages, &Kernel.send(pid, &1))
144 |
145 | {:reply, :ok, state}
146 | end
147 |
148 | @impl true
149 | def handle_cast(cast, state)
150 |
151 | def handle_cast(:close, %State{} = state) do
152 | {:stop, :normal, state}
153 | end
154 |
155 | def handle_cast(:resend_connect_ack, %State{} = state) do
156 | udp_send(state, "/ack/#{state.session_id}/#{state.in_position}/")
157 | {:noreply, state}
158 | end
159 |
160 | def handle_cast({:handle_packet, {:data, _session_id, position, data}}, %State{} = state) do
161 | state = reset_idle_timer(state)
162 |
163 | if position == state.in_position do
164 | unescaped_data = unescape_data(data)
165 | state = update_in(state.in_position, &(&1 + byte_size(unescaped_data)))
166 | udp_send(state, "/ack/#{state.session_id}/#{state.in_position}/")
167 | state = send_or_queue_message(state, {:lrcp, %__MODULE__{pid: self()}, unescaped_data})
168 | {:noreply, state}
169 | else
170 | # If we're not caught up, we resend the ack with the position of where
171 | # we're caught up and keep going.
172 | udp_send(state, "/ack/#{state.session_id}/#{state.in_position}/")
173 | {:noreply, state}
174 | end
175 | end
176 |
177 | def handle_cast({:handle_packet, {:ack, _session_id, length}}, %State{} = state) do
178 | cond do
179 | length <= state.acked_out_position ->
180 | # Do nothing and stop, it's probably a duplicate ack.
181 | Logger.debug("Ignoring ack for #{length} bytes, we've already acked that")
182 | {:noreply, state}
183 |
184 | length > state.out_position ->
185 | # Client is misbehaving, close the session.
186 | Logger.debug(
187 | "Client is misbehaving, closing session (sent ack for position #{length} " <>
188 | "but we've only sent #{state.out_position} bytes)"
189 | )
190 |
191 | udp_send(state, "/close/#{state.session_id}/")
192 |
193 | state =
194 | send_or_queue_message(
195 | state,
196 | {:lrcp_error, %__MODULE__{pid: self()}, :client_misbehaving}
197 | )
198 |
199 | {:stop, :normal, state}
200 |
201 | length < state.acked_out_position + byte_size(state.pending_out_payload) ->
202 | transmitted_bytes = length - state.acked_out_position
203 | Logger.debug("Partial ack for #{transmitted_bytes} bytes")
204 |
205 | still_pending_payload =
206 | :binary.part(
207 | state.pending_out_payload,
208 | transmitted_bytes,
209 | byte_size(state.pending_out_payload) - transmitted_bytes
210 | )
211 |
212 | udp_send(
213 | state,
214 | "/data/#{state.session_id}/#{state.acked_out_position + transmitted_bytes}/" <>
215 | escape_data(still_pending_payload) <> "/"
216 | )
217 |
218 | state = put_in(state.acked_out_position, length)
219 | state = put_in(state.pending_out_payload, still_pending_payload)
220 | {:noreply, state}
221 |
222 | length == state.out_position ->
223 | Logger.debug("Everything we've sent has been acked")
224 | state = put_in(state.acked_out_position, length)
225 | state = put_in(state.pending_out_payload, <<>>)
226 | {:noreply, state}
227 |
228 | true ->
229 | raise """
230 | Should never reach this.
231 |
232 | state: #{inspect(state)}
233 | length: #{length}
234 | """
235 | end
236 | end
237 |
238 | ## Helpers
239 |
240 | defp send_data(%State{} = state, <<>>) do
241 | Process.send_after(self(), :retransmit_pending_data, @retransmit_interval)
242 | state
243 | end
244 |
245 | defp send_data(%State{} = state, data) do
246 | {chunk, rest} =
247 | case data do
248 | <> -> {chunk, rest}
249 | chunk -> {chunk, ""}
250 | end
251 |
252 | udp_send(state, "/data/#{state.session_id}/#{state.out_position}/#{escape_data(chunk)}/")
253 | state = update_in(state.out_position, &(&1 + byte_size(chunk)))
254 |
255 | send_data(state, rest)
256 | end
257 |
258 | defp send_or_queue_message(%State{} = state, message) do
259 | if state.controlling_process do
260 | Logger.debug("Sending message to client: #{inspect(message)}")
261 | Kernel.send(state.controlling_process, message)
262 | state
263 | else
264 | Logger.debug("Queueing message: #{inspect(message)}")
265 | update_in(state.out_message_queue, &:queue.in(message, &1))
266 | end
267 | end
268 |
269 | defp escape_data(data) do
270 | data
271 | |> String.replace("\\", "\\\\")
272 | |> String.replace("/", "\\/")
273 | end
274 |
275 | defp unescape_data(data) do
276 | data
277 | |> String.replace("\\/", "/")
278 | |> String.replace("\\\\", "\\")
279 | end
280 |
281 | defp udp_send(%State{} = state, data) do
282 | Logger.debug("--> #{inspect(data)}")
283 | :ok = :gen_udp.send(state.udp_socket, state.peer_ip, state.peer_port, data)
284 | end
285 |
286 | defp reset_idle_timer(%State{} = state) do
287 | Process.cancel_timer(state.idle_timer_ref)
288 | idle_timer_ref = Process.send_after(self(), :idle_timeout, @idle_timeout)
289 | put_in(state.idle_timer_ref, idle_timer_ref)
290 | end
291 | end
292 |
--------------------------------------------------------------------------------
/apps/line_reversal/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule LineReversal.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :line_reversal,
7 | version: "0.1.0",
8 | build_path: "../../_build",
9 | config_path: "../../config/config.exs",
10 | deps_path: "../../deps",
11 | lockfile: "../../mix.lock",
12 | elixir: "~> 1.14",
13 | start_permanent: Mix.env() == :prod,
14 | deps: deps()
15 | ]
16 | end
17 |
18 | # Run "mix help compile.app" to learn about applications.
19 | def application do
20 | [
21 | extra_applications: [:logger],
22 | mod: {LineReversal.Application, []}
23 | ]
24 | end
25 |
26 | # Run "mix help deps" to learn about dependencies.
27 | defp deps do
28 | [
29 | # {:dep_from_hexpm, "~> 0.3.0"},
30 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
31 | # {:sibling_app_in_umbrella, in_umbrella: true}
32 | ]
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/apps/line_reversal/mix.lock:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whatyouhide/protohackers_in_elixir/f73015610479467c0d60fcaeda6da8dbcc4d6538/apps/line_reversal/mix.lock
--------------------------------------------------------------------------------
/apps/line_reversal/rel/env.bat.eex:
--------------------------------------------------------------------------------
1 | @echo off
2 | rem Set the release to load code on demand (interactive) instead of preloading (embedded).
3 | rem set RELEASE_MODE=interactive
4 |
5 | rem Set the release to work across nodes.
6 | rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
7 | rem set RELEASE_DISTRIBUTION=name
8 | rem set RELEASE_NODE=<%= @release.name %>
9 |
--------------------------------------------------------------------------------
/apps/line_reversal/rel/env.sh.eex:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ip=$(grep fly-local-6pn /etc/hosts | cut -f 1)
4 | export RELEASE_DISTRIBUTION=name
5 | export RELEASE_NODE=$FLY_APP_NAME@"$ip"
6 | export ELIXIR_ERL_OPTIONS="-proto_dist inet6_tcp"
7 |
--------------------------------------------------------------------------------
/apps/line_reversal/rel/remote.vm.args.eex:
--------------------------------------------------------------------------------
1 | ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html
2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here
3 |
4 | ## Increase number of concurrent ports/sockets
5 | ##+Q 65536
6 |
7 | ## Tweak GC to run more often
8 | ##-env ERL_FULLSWEEP_AFTER 10
9 |
--------------------------------------------------------------------------------
/apps/line_reversal/rel/vm.args.eex:
--------------------------------------------------------------------------------
1 | ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html
2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here
3 |
4 | ## Increase number of concurrent ports/sockets
5 | ##+Q 65536
6 |
7 | ## Tweak GC to run more often
8 | ##-env ERL_FULLSWEEP_AFTER 10
9 |
--------------------------------------------------------------------------------
/apps/line_reversal/test/line_reversal/lrcp/protocol_test.exs:
--------------------------------------------------------------------------------
1 | defmodule LineReversal.LRCP.ProtocolTest do
2 | use ExUnit.Case, async: true
3 |
4 | import LineReversal.LRCP.Protocol
5 |
6 | @max_int 2_147_483_648
7 |
8 | describe "parse_packet/1" do
9 | test "invalid packets" do
10 | assert parse_packet("") == :error
11 | assert parse_packet("/") == :error
12 | assert parse_packet("//") == :error
13 | assert parse_packet("/connect") == :error
14 | assert parse_packet("/connect/1") == :error
15 | assert parse_packet("connect/1/") == :error
16 | end
17 |
18 | test "returns an error for integers that are too large" do
19 | assert parse_packet("/connect/#{@max_int}/") == :error
20 | assert parse_packet("/ack/#{@max_int}/1/") == :error
21 | assert parse_packet("/ack/1/#{@max_int}/") == :error
22 | end
23 |
24 | test "connect packet" do
25 | assert parse_packet("/connect/231/") == {:ok, {:connect, 231}}
26 | end
27 |
28 | test "close packet" do
29 | assert parse_packet("/close/231/") == {:ok, {:close, 231}}
30 | end
31 |
32 | test "ack packet" do
33 | assert parse_packet("/ack/123/456/") == {:ok, {:ack, 123, 456}}
34 | end
35 |
36 | test "data packet" do
37 | assert parse_packet("/data/123/456/hello\\/world\\\\!\n/") ==
38 | {:ok, {:data, 123, 456, "hello\\/world\\\\!\n"}}
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/apps/line_reversal/test/line_reversal/udp_server_test.exs:
--------------------------------------------------------------------------------
1 | defmodule LineReversal.UDPServerTest do
2 | use ExUnit.Case, async: true
3 |
4 | test "connecting and closing multiple clients" do
5 | {client1, session_id1} = open_udp()
6 | {client2, session_id2} = open_udp()
7 |
8 | udp_send(client1, "/connect/#{session_id1}/")
9 | udp_send(client2, "/connect/#{session_id2}/")
10 |
11 | assert udp_recv(client1) == "/ack/#{session_id1}/0/"
12 | assert udp_recv(client2) == "/ack/#{session_id2}/0/"
13 |
14 | # Closing
15 |
16 | udp_send(client1, "/close/#{session_id1}/")
17 | assert udp_recv(client1) == "/close/#{session_id1}/"
18 |
19 | udp_send(client2, "/close/#{session_id2}/")
20 | assert udp_recv(client2) == "/close/#{session_id2}/"
21 | end
22 |
23 | test "server ignores invalid messages" do
24 | {client, session_id} = open_udp()
25 |
26 | udp_send(client, "invalid")
27 | udp_send(client, "/connect/1")
28 | udp_send(client, "connect/1/")
29 | udp_send(client, "/ack/")
30 | udp_send(client, "//")
31 |
32 | # We can still connect after invalid messages
33 | udp_send(client, "/connect/#{session_id}/")
34 | assert udp_recv(client) == "/ack/#{session_id}/0/"
35 | end
36 |
37 | test "servers sends CLOSE if sending a packet to a dead client" do
38 | {client, session_id} = open_udp()
39 |
40 | # Connect
41 | udp_send(client, "/connect/#{session_id}/")
42 | assert udp_recv(client) == "/ack/#{session_id}/0/"
43 |
44 | # Close
45 | udp_send(client, "/close/#{session_id}/")
46 | assert udp_recv(client) == "/close/#{session_id}/"
47 |
48 | # Send a packet to a dead client and get another close message
49 | udp_send(client, "/data/#{session_id}/1/hello/")
50 | assert udp_recv(client) == "/close/#{session_id}/"
51 | end
52 |
53 | test "server receives data and sends the appropriate acks" do
54 | {client, session_id} = open_udp()
55 |
56 | # Connect
57 | udp_send(client, "/connect/#{session_id}/")
58 | assert udp_recv(client) == "/ack/#{session_id}/0/"
59 |
60 | # If we send a packet with invalid position (greater than 0 initially), we receive
61 | # an ack for 0.
62 | udp_send(client, "/data/#{session_id}/1/hello/")
63 | assert udp_recv(client) == "/ack/#{session_id}/0/"
64 |
65 | # Now we send real data at the right position, we should get the right ack.
66 | udp_send(client, "/data/#{session_id}/0/hello/")
67 | assert udp_recv(client) == "/ack/#{session_id}/5/"
68 |
69 | # If we send more data, we should get the right ack again.
70 | udp_send(client, "/data/#{session_id}/5/\\//")
71 | assert udp_recv(client) == "/ack/#{session_id}/6/"
72 |
73 | # If we send data with the wrong position, we get the ack for the last position.
74 | udp_send(client, "/data/#{session_id}/3/wrongpos/")
75 | assert udp_recv(client) == "/ack/#{session_id}/6/"
76 | end
77 |
78 | test "example session from the problem" do
79 | assert {:ok, client} = :gen_udp.open(0, [:binary, active: false])
80 |
81 | # Connect
82 | udp_send(client, "/connect/12345/")
83 | assert udp_recv(client) == "/ack/12345/0/"
84 |
85 | udp_send(client, "/data/12345/0/hello\n/")
86 | assert udp_recv(client) == "/ack/12345/6/"
87 |
88 | assert udp_recv(client) == "/data/12345/0/olleh\n/"
89 | udp_send(client, "/ack/12345/6/")
90 |
91 | udp_send(client, "/data/12345/6/Hello, world!\n/")
92 | assert udp_recv(client) == "/ack/12345/20/"
93 |
94 | assert udp_recv(client) == "/data/12345/6/!dlrow ,olleH\n/"
95 |
96 | udp_send(client, "/ack/12345/20/")
97 |
98 | udp_send(client, "/close/12345/")
99 | assert udp_recv(client) == "/close/12345/"
100 | end
101 |
102 | test "closing the same session multiple times" do
103 | {client, session_id} = open_udp()
104 | udp_send(client, "/connect/#{session_id}/")
105 | assert udp_recv(client) == "/ack/#{session_id}/0/"
106 |
107 | udp_send(client, "/close/#{session_id}/")
108 | assert udp_recv(client) == "/close/#{session_id}/"
109 | udp_send(client, "/close/#{session_id}/")
110 | assert udp_recv(client) == "/close/#{session_id}/"
111 | end
112 |
113 | test "data sent in broken packets" do
114 | {client, session_id} = open_udp()
115 | udp_send(client, "/connect/#{session_id}/")
116 | assert udp_recv(client) == "/ack/#{session_id}/0/"
117 |
118 | udp_send(client, "/data/#{session_id}/0/hello /")
119 | assert udp_recv(client) == "/ack/#{session_id}/6/"
120 |
121 | udp_send(client, "/data/#{session_id}/6/world!/")
122 | assert udp_recv(client) == "/ack/#{session_id}/12/"
123 |
124 | udp_send(client, "/data/#{session_id}/12/\\/\n/")
125 | assert udp_recv(client) == "/ack/#{session_id}/14/"
126 |
127 | assert udp_recv(client) == "/data/#{session_id}/0/\\/#{String.reverse("hello world!")}\n/"
128 | end
129 |
130 | test "multiple sessions in parallel" do
131 | tasks =
132 | for _ <- 1..20 do
133 | session_id = System.unique_integer([:positive])
134 |
135 | Task.async(fn ->
136 | assert {:ok, client} = :gen_udp.open(0, [:binary, active: false])
137 | udp_send(client, "/connect/#{session_id}/")
138 | assert udp_recv(client) == "/ack/#{session_id}/0/"
139 |
140 | udp_send(client, "/data/#{session_id}/0/hello\n/")
141 | assert udp_recv(client) == "/ack/#{session_id}/6/"
142 |
143 | assert udp_recv(client) == "/data/#{session_id}/0/olleh\n/"
144 | udp_send(client, "/ack/#{session_id}/6/")
145 |
146 | udp_send(client, "/close/#{session_id}/")
147 | assert udp_recv(client) == "/close/#{session_id}/"
148 |
149 | :done
150 | end)
151 | end
152 |
153 | assert Enum.all?(Task.yield_many(tasks, 5000), &match?({_task, {:ok, :done}}, &1))
154 | end
155 |
156 | test "multiple lines in a single UDP packet" do
157 | {client, session_id} = open_udp()
158 | udp_send(client, "/connect/#{session_id}/")
159 | assert udp_recv(client) == "/ack/#{session_id}/0/"
160 |
161 | udp_send(client, "/data/#{session_id}/0/abcd\n1234\nfoo/")
162 | assert udp_recv(client) == "/ack/#{session_id}/13/"
163 |
164 | assert udp_recv(client) == "/data/#{session_id}/0/dcba\n/"
165 | assert udp_recv(client) == "/data/#{session_id}/5/4321\n/"
166 | end
167 |
168 | test "sending and receiving big data" do
169 | {client, session_id} = open_udp()
170 | udp_send(client, "/connect/#{session_id}/")
171 | assert udp_recv(client) == "/ack/#{session_id}/0/"
172 |
173 | data_to_send = :binary.copy("a", 700)
174 |
175 | udp_send(client, "/data/#{session_id}/0/#{data_to_send}/")
176 | assert udp_recv(client) == "/ack/#{session_id}/700/"
177 | udp_send(client, "/data/#{session_id}/700/#{data_to_send}\n/")
178 | assert udp_recv(client) == "/ack/#{session_id}/1401/"
179 |
180 | assert udp_recv(client) == "/data/#{session_id}/0/#{:binary.copy("a", 971)}/"
181 | assert udp_recv(client) == "/data/#{session_id}/971/#{:binary.copy("a", 1400 - 971)}\n/"
182 | end
183 |
184 | @tag :capture_log
185 | test "server sends close message if clients misbehaves with acks and positions" do
186 | {client, session_id} = open_udp()
187 | udp_send(client, "/connect/#{session_id}/")
188 | assert udp_recv(client) == "/ack/#{session_id}/0/"
189 |
190 | len = String.length("either/or\n")
191 |
192 | udp_send(client, "/data/#{session_id}/0/either\\/or\n/")
193 | assert udp_recv(client) == "/ack/#{session_id}/#{len}/"
194 |
195 | assert udp_recv(client) == "/data/#{session_id}/0/ro\\/rehtie\n/"
196 |
197 | # Ack only two bytes, which means that the server should resend us the rest of the data.
198 | udp_send(client, "/ack/#{session_id}/2/")
199 | assert udp_recv(client) == "/data/#{session_id}/2/\\/rehtie\n/"
200 |
201 | # Ack another two bytes and let the server resend the rest.
202 | udp_send(client, "/ack/#{session_id}/3/")
203 | assert udp_recv(client) == "/data/#{session_id}/3/rehtie\n/"
204 |
205 | # If we ack something we already acked again, nothing happens.
206 | udp_send(client, "/ack/#{session_id}/1/")
207 |
208 | # If we hack further than what the server sent us, it's a protocol error.
209 | udp_send(client, "/ack/#{session_id}/1000/")
210 | assert udp_recv(client) == "/close/#{session_id}/"
211 |
212 | Process.sleep(100)
213 | end
214 |
215 | test "server retrasmits data if it doesn't receive acks" do
216 | {client, session_id} = open_udp()
217 |
218 | udp_send(client, "/connect/#{session_id}/")
219 | assert udp_recv(client) == "/ack/#{session_id}/0/"
220 |
221 | udp_send(client, "/data/#{session_id}/0/hello\n/")
222 | assert udp_recv(client) == "/ack/#{session_id}/6/"
223 |
224 | assert udp_recv(client) == "/data/#{session_id}/0/olleh\n/"
225 | assert udp_recv(client) == "/data/#{session_id}/0/olleh\n/"
226 | assert udp_recv(client) == "/data/#{session_id}/0/olleh\n/"
227 |
228 | udp_send(client, "/ack/#{session_id}/3/")
229 | assert udp_recv(client) == "/data/#{session_id}/3/eh\n/"
230 | assert udp_recv(client) == "/data/#{session_id}/3/eh\n/"
231 | end
232 |
233 | ## Helper
234 |
235 | defp open_udp do
236 | session_id = System.unique_integer([:positive])
237 | assert {:ok, socket} = :gen_udp.open(0, [:binary, active: false])
238 | {socket, session_id}
239 | end
240 |
241 | defp udp_recv(client) do
242 | assert {:ok, {_ip, _port, data}} = :gen_udp.recv(client, 0, 2_000)
243 | data
244 | end
245 |
246 | defp udp_send(client, data) do
247 | assert :ok = :gen_udp.send(client, {127, 0, 0, 1}, 5008, data)
248 | end
249 | end
250 |
--------------------------------------------------------------------------------
/apps/line_reversal/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/lib/protohackers/application.ex:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.Application do
2 | # See https://hexdocs.pm/elixir/Application.html
3 | # for more information on OTP Applications
4 | @moduledoc false
5 |
6 | use Application
7 |
8 | @impl true
9 | def start(_type, _args) do
10 | children = [
11 | {Protohackers.EchoServer, port: 5001},
12 | {Protohackers.PrimeServer, port: 5002},
13 | {Protohackers.PricesServer, port: 5003},
14 | {Protohackers.BudgetChatServer, port: 5004},
15 | {Protohackers.UDPServer, port: 5005},
16 | {Protohackers.MITM.Supervisor, port: 5006},
17 | ]
18 |
19 | # See https://hexdocs.pm/elixir/Supervisor.html
20 | # for other strategies and supported options
21 | opts = [strategy: :one_for_one, name: Protohackers.Supervisor]
22 | Supervisor.start_link(children, opts)
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/lib/protohackers/budget_chat_server.ex:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.BudgetChatServer do
2 | use GenServer
3 |
4 | require Logger
5 |
6 | @spec start_link(keyword()) :: GenServer.on_start()
7 | def start_link(opts) do
8 | GenServer.start_link(__MODULE__, opts)
9 | end
10 |
11 | defstruct [:listen_socket, :supervisor, :ets]
12 |
13 | @impl true
14 | def init(opts) do
15 | port = Keyword.fetch!(opts, :port)
16 | {:ok, supervisor} = Task.Supervisor.start_link(max_children: 100)
17 |
18 | ets = :ets.new(__MODULE__, [:public])
19 |
20 | listen_options = [
21 | ifaddr: {0, 0, 0, 0},
22 | mode: :binary,
23 | active: false,
24 | reuseaddr: true,
25 | exit_on_close: false,
26 | packet: :line,
27 | buffer: 1024 * 100
28 | ]
29 |
30 | case :gen_tcp.listen(port, listen_options) do
31 | {:ok, listen_socket} ->
32 | Logger.info("Started server on port #{port}")
33 | state = %__MODULE__{listen_socket: listen_socket, supervisor: supervisor, ets: ets}
34 | {:ok, state, {:continue, :accept}}
35 |
36 | {:error, reason} ->
37 | {:stop, reason}
38 | end
39 | end
40 |
41 | @impl true
42 | def handle_continue(:accept, %__MODULE__{} = state) do
43 | case :gen_tcp.accept(state.listen_socket) do
44 | {:ok, socket} ->
45 | Task.Supervisor.start_child(state.supervisor, fn ->
46 | handle_connection(socket, state.ets)
47 | end)
48 |
49 | {:noreply, state, {:continue, :accept}}
50 |
51 | {:error, reason} ->
52 | {:stop, reason}
53 | end
54 | end
55 |
56 | ## Helpers
57 |
58 | defp handle_connection(socket, ets) do
59 | :ok = :gen_tcp.send(socket, "What's your username?\n")
60 |
61 | case :gen_tcp.recv(socket, 0, 300_000) do
62 | {:ok, line} ->
63 | username = String.trim(line)
64 |
65 | if username =~ ~r/^[[:alnum:]]+$/ do
66 | Logger.debug("Username #{username} connected")
67 | all_users = :ets.match(ets, :"$1")
68 | usernames = Enum.map_join(all_users, ", ", fn [{_socket, username}] -> username end)
69 | :ets.insert(ets, {socket, username})
70 |
71 | Enum.each(all_users, fn [{socket, _username}] ->
72 | :gen_tcp.send(socket, "* #{username} has entered the chat\n")
73 | end)
74 |
75 | :ok = :gen_tcp.send(socket, "* The room contains: #{usernames}\n")
76 | handle_chat_session(socket, ets, username)
77 | else
78 | :ok = :gen_tcp.send(socket, "Invalid username\n")
79 | :gen_tcp.close(socket)
80 | end
81 |
82 | {:error, _reason} ->
83 | :gen_tcp.close(socket)
84 | :ok
85 | end
86 | end
87 |
88 | def handle_chat_session(socket, ets, username) do
89 | case :gen_tcp.recv(socket, 0, 300_000) do
90 | {:ok, message} ->
91 | message = String.trim(message)
92 |
93 | if message != "" do
94 | all_sockets = :ets.match(ets, {:"$1", :_})
95 |
96 | for [other_socket] <- all_sockets, other_socket != socket do
97 | :gen_tcp.send(other_socket, "[#{username}] #{message}\n")
98 | end
99 | end
100 |
101 | handle_chat_session(socket, ets, username)
102 |
103 | {:error, _reason} ->
104 | all_sockets = :ets.match(ets, {:"$1", :_})
105 |
106 | for [other_socket] <- all_sockets, other_socket != socket do
107 | :gen_tcp.send(other_socket, "* #{username} left\n")
108 | end
109 |
110 | _ = :gen_tcp.close(socket)
111 | :ets.delete(ets, socket)
112 | end
113 | end
114 | end
115 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/lib/protohackers/echo_server.ex:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.EchoServer do
2 | use GenServer
3 |
4 | require Logger
5 |
6 | @spec start_link(keyword()) :: GenServer.on_start()
7 | def start_link(opts) do
8 | GenServer.start_link(__MODULE__, opts)
9 | end
10 |
11 | defstruct [:listen_socket, :supervisor]
12 |
13 | @impl true
14 | def init(opts) do
15 | port = Keyword.fetch!(opts, :port)
16 | {:ok, supervisor} = Task.Supervisor.start_link(max_children: 100)
17 |
18 | listen_options = [
19 | ifaddr: {0, 0, 0, 0},
20 | mode: :binary,
21 | active: false,
22 | reuseaddr: true,
23 | exit_on_close: false
24 | ]
25 |
26 | case :gen_tcp.listen(port, listen_options) do
27 | {:ok, listen_socket} ->
28 | Logger.info("Staretd server on port #{port}")
29 | state = %__MODULE__{listen_socket: listen_socket, supervisor: supervisor}
30 | {:ok, state, {:continue, :accept}}
31 |
32 | {:error, reason} ->
33 | {:stop, reason}
34 | end
35 | end
36 |
37 | @impl true
38 | def handle_continue(:accept, %__MODULE__{} = state) do
39 | case :gen_tcp.accept(state.listen_socket) do
40 | {:ok, socket} ->
41 | Task.Supervisor.start_child(state.supervisor, fn -> handle_connection(socket) end)
42 | {:noreply, state, {:continue, :accept}}
43 |
44 | {:error, reason} ->
45 | {:stop, reason}
46 | end
47 | end
48 |
49 | ## Helpers
50 |
51 | defp handle_connection(socket) do
52 | case recv_until_closed(socket, _buffer = "", _buffered_size = 0) do
53 | {:ok, data} -> :gen_tcp.send(socket, data)
54 | {:error, reason} -> Logger.error("Failed to receive data: #{inspect(reason)}")
55 | end
56 |
57 | :gen_tcp.close(socket)
58 | end
59 |
60 | @limit _100_kb = 1024 * 100
61 |
62 | defp recv_until_closed(socket, buffer, buffered_size) do
63 | case :gen_tcp.recv(socket, 0, 10_000) do
64 | {:ok, data} when buffered_size + byte_size(data) > @limit -> {:error, :buffer_overflow}
65 | {:ok, data} -> recv_until_closed(socket, [buffer, data], buffered_size + byte_size(data))
66 | {:error, :closed} -> {:ok, buffer}
67 | {:error, reason} -> {:error, reason}
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/lib/protohackers/mitm/acceptor.ex:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.MITM.Acceptor do
2 | use Task, restart: :transient
3 |
4 | require Logger
5 |
6 | @spec start_link(keyword()) :: GenServer.on_start()
7 | def start_link(opts) do
8 | Task.start_link(__MODULE__, :run, [Keyword.fetch!(opts, :port)])
9 | end
10 |
11 | @spec run(:inet.port_number()) :: no_return()
12 | def run(port) do
13 | case :gen_tcp.listen(port, [
14 | :binary,
15 | ifaddr: {0, 0, 0, 0},
16 | active: :once,
17 | packet: :line,
18 | reuseaddr: true
19 | ]) do
20 | {:ok, listen_socket} ->
21 | Logger.info("MITM server listening on port #{port}")
22 | accept_loop(listen_socket)
23 |
24 | {:error, reason} ->
25 | raise "failed to listen on port #{port}: #{inspect(reason)}"
26 | end
27 | end
28 |
29 | defp accept_loop(listen_socket) do
30 | case :gen_tcp.accept(listen_socket) do
31 | {:ok, socket} ->
32 | {:ok, _} = Protohackers.MITM.ConnectionSupervisor.start_child(socket)
33 | accept_loop(listen_socket)
34 |
35 | {:error, reason} ->
36 | raise "failed to accept connection: #{inspect(reason)}"
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/lib/protohackers/mitm/boguscoin.ex:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.MITM.Boguscoin do
2 | @tonys_address "7YWHMfk9JZe0LM0g1ZauHuiSxhI"
3 |
4 | @spec rewrite_addresses(binary()) :: binary()
5 | def rewrite_addresses(string) when is_binary(string) do
6 | regex = ~r/(^|\s)\K(7[[:alnum:]]{25,34})(?= [^[:alnum:]]|\s|$)/
7 | Regex.replace(regex, string, @tonys_address)
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/lib/protohackers/mitm/connection.ex:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.MITM.Connection do
2 | use GenServer
3 |
4 | require Logger
5 |
6 | @spec start_link(:gen_tcp.socket()) :: GenServer.on_start()
7 | def start_link(incoming_socket) do
8 | GenServer.start_link(__MODULE__, incoming_socket)
9 | end
10 |
11 | defstruct [:incoming_socket, :outgoing_socket]
12 |
13 | @impl true
14 | def init(incoming_socket) do
15 | case :gen_tcp.connect(~c"chat.protohackers.com", 16963, [:binary, active: :once]) do
16 | {:ok, outgoing_socket} ->
17 | Logger.debug("Started connection handler")
18 | {:ok, %__MODULE__{incoming_socket: incoming_socket, outgoing_socket: outgoing_socket}}
19 |
20 | {:error, reason} ->
21 | Logger.error("Failed to connect to the upstream server: #{inspect(reason)}")
22 | {:stop, reason}
23 | end
24 | end
25 |
26 | @impl true
27 | def handle_info(message, state)
28 |
29 | def handle_info(
30 | {:tcp, incoming_socket, data},
31 | %__MODULE__{incoming_socket: incoming_socket} = state
32 | ) do
33 | :ok = :inet.setopts(incoming_socket, active: :once)
34 | Logger.debug("Received data: #{inspect(data)}")
35 | data = Protohackers.MITM.Boguscoin.rewrite_addresses(data)
36 | :gen_tcp.send(state.outgoing_socket, data)
37 | {:noreply, state}
38 | end
39 |
40 | def handle_info(
41 | {:tcp, outgoing_socket, data},
42 | %__MODULE__{outgoing_socket: outgoing_socket} = state
43 | ) do
44 | :ok = :inet.setopts(outgoing_socket, active: :once)
45 | Logger.debug("Received data: #{inspect(data)}")
46 | data = Protohackers.MITM.Boguscoin.rewrite_addresses(data)
47 | :gen_tcp.send(state.incoming_socket, data)
48 | {:noreply, state}
49 | end
50 |
51 | def handle_info({:tcp_error, socket, reason}, %__MODULE__{} = state)
52 | when socket in [state.incoming_socket, state.outgoing_socket] do
53 | Logger.error("Received TCP error: #{inspect(reason)}")
54 | :gen_tcp.close(state.incoming_socket)
55 | :gen_tcp.close(state.outgoing_socket)
56 | {:stop, :normal, state}
57 | end
58 |
59 | def handle_info({:tcp_closed, socket}, %__MODULE__{} = state)
60 | when socket in [state.incoming_socket, state.outgoing_socket] do
61 | Logger.debug("TCP connection closed")
62 | :gen_tcp.close(state.incoming_socket)
63 | :gen_tcp.close(state.outgoing_socket)
64 | {:stop, :normal, state}
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/lib/protohackers/mitm/connection_supervisor.ex:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.MITM.ConnectionSupervisor do
2 | use DynamicSupervisor
3 |
4 | @spec start_link(keyword()) :: Supervisor.on_start()
5 | def start_link([] = _opts) do
6 | DynamicSupervisor.start_link(__MODULE__, :no_args, name: __MODULE__)
7 | end
8 |
9 | @spec start_child(:gen_tcp.socket()) :: DynamicSupervisor.on_start_child()
10 | def start_child(socket) do
11 | child_spec = {Protohackers.MITM.Connection, socket}
12 |
13 | with {:ok, conn} <- DynamicSupervisor.start_child(__MODULE__, child_spec),
14 | :ok <- :gen_tcp.controlling_process(socket, conn) do
15 | {:ok, conn}
16 | end
17 | end
18 |
19 | @impl true
20 | def init(:no_args) do
21 | DynamicSupervisor.init(strategy: :one_for_one, max_children: 50)
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/lib/protohackers/mitm/supervisor.ex:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.MITM.Supervisor do
2 | use Supervisor
3 |
4 | @spec start_link(keyword()) :: Supervisor.on_start()
5 | def start_link(opts) do
6 | Supervisor.start_link(__MODULE__, opts)
7 | end
8 |
9 | @impl true
10 | def init(opts) do
11 | children = [
12 | {Protohackers.MITM.ConnectionSupervisor, []},
13 | {Protohackers.MITM.Acceptor, opts}
14 | ]
15 |
16 | Supervisor.init(children, strategy: :rest_for_one)
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/lib/protohackers/prices_server.ex:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.PricesServer do
2 | use GenServer
3 |
4 | alias Protohackers.PricesServer.DB
5 |
6 | require Logger
7 |
8 | @spec start_link(keyword()) :: GenServer.on_start()
9 | def start_link(opts) do
10 | GenServer.start_link(__MODULE__, opts)
11 | end
12 |
13 | defstruct [:listen_socket, :supervisor]
14 |
15 | @impl true
16 | def init(opts) do
17 | port = Keyword.fetch!(opts, :port)
18 | {:ok, supervisor} = Task.Supervisor.start_link(max_children: 100)
19 |
20 | listen_options = [
21 | ifaddr: {0, 0, 0, 0},
22 | mode: :binary,
23 | active: false,
24 | reuseaddr: true,
25 | exit_on_close: false,
26 | backlog: 100
27 | ]
28 |
29 | case :gen_tcp.listen(port, listen_options) do
30 | {:ok, listen_socket} ->
31 | Logger.info("Started server on port #{port}")
32 | state = %__MODULE__{listen_socket: listen_socket, supervisor: supervisor}
33 | {:ok, state, {:continue, :accept}}
34 |
35 | {:error, reason} ->
36 | {:stop, reason}
37 | end
38 | end
39 |
40 | @impl true
41 | def handle_continue(:accept, %__MODULE__{} = state) do
42 | case :gen_tcp.accept(state.listen_socket) do
43 | {:ok, socket} ->
44 | Task.Supervisor.start_child(state.supervisor, fn -> handle_connection(socket) end)
45 | {:noreply, state, {:continue, :accept}}
46 |
47 | {:error, reason} ->
48 | {:stop, reason}
49 | end
50 | end
51 |
52 | ## Helpers
53 |
54 | defp handle_connection(socket) do
55 | case handle_requests(socket, DB.new()) do
56 | :ok -> :ok
57 | {:error, reason} -> Logger.error("Failed to receive data: #{inspect(reason)}")
58 | end
59 |
60 | :gen_tcp.close(socket)
61 | end
62 |
63 | defp handle_requests(socket, db) do
64 | case :gen_tcp.recv(socket, 9, 10_000) do
65 | {:ok, data} ->
66 | case handle_request(data, db) do
67 | {nil, db} ->
68 | handle_requests(socket, db)
69 |
70 | {response, db} ->
71 | :gen_tcp.send(socket, response)
72 | handle_requests(socket, db)
73 |
74 | :error ->
75 | {:error, :invalid_request}
76 | end
77 |
78 | {:error, :timeout} ->
79 | handle_requests(socket, db)
80 |
81 | {:error, :closed} ->
82 | :ok
83 |
84 | {:error, reason} ->
85 | {:error, reason}
86 | end
87 | end
88 |
89 | defp handle_request(<>, db) do
90 | {nil, DB.add(db, timestamp, price)}
91 | end
92 |
93 | defp handle_request(<>, db) do
94 | avg = DB.query(db, mintime, maxtime)
95 | {<>, db}
96 | end
97 |
98 | defp handle_request(_other, _db) do
99 | :error
100 | end
101 | end
102 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/lib/protohackers/prices_server/db.ex:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.PricesServer.DB do
2 | @type timestamp() :: integer()
3 | @type price() :: integer()
4 | @type t() :: [{timestamp(), price()}]
5 |
6 | @spec new() :: t()
7 | def new do
8 | []
9 | end
10 |
11 | @spec add(t(), timestamp(), price()) :: t()
12 | def add(db, timestamp, price)
13 | when is_list(db) and is_integer(timestamp) and is_integer(price) do
14 | [{timestamp, price} | db]
15 | end
16 |
17 | @spec query(t(), timestamp(), timestamp()) :: price()
18 | def query(db, from, to) when is_list(db) and is_integer(from) and is_integer(to) do
19 | db
20 | |> Stream.filter(fn {timestamp, _price} -> timestamp >= from and timestamp <= to end)
21 | |> Stream.map(fn {_timestamp, price} -> price end)
22 | |> Enum.reduce({0, 0}, fn price, {sum, count} -> {sum + price, count + 1} end)
23 | |> then(fn
24 | {_sum, 0} -> 0
25 | {sum, count} -> div(sum, count)
26 | end)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/lib/protohackers/prime_server.ex:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.PrimeServer do
2 | use GenServer
3 |
4 | require Logger
5 |
6 | @spec start_link(keyword()) :: GenServer.on_start()
7 | def start_link(opts) do
8 | GenServer.start_link(__MODULE__, opts)
9 | end
10 |
11 | defstruct [:listen_socket, :supervisor]
12 |
13 | @impl true
14 | def init(opts) do
15 | port = Keyword.fetch!(opts, :port)
16 | {:ok, supervisor} = Task.Supervisor.start_link(max_children: 100)
17 |
18 | listen_options = [
19 | ifaddr: {0, 0, 0, 0},
20 | mode: :binary,
21 | active: false,
22 | reuseaddr: true,
23 | exit_on_close: false,
24 | packet: :line,
25 | buffer: 1024 * 100
26 | ]
27 |
28 | case :gen_tcp.listen(port, listen_options) do
29 | {:ok, listen_socket} ->
30 | Logger.info("Started server on port #{port}")
31 | state = %__MODULE__{listen_socket: listen_socket, supervisor: supervisor}
32 | {:ok, state, {:continue, :accept}}
33 |
34 | {:error, reason} ->
35 | {:stop, reason}
36 | end
37 | end
38 |
39 | @impl true
40 | def handle_continue(:accept, %__MODULE__{} = state) do
41 | case :gen_tcp.accept(state.listen_socket) do
42 | {:ok, socket} ->
43 | Task.Supervisor.start_child(state.supervisor, fn -> handle_connection(socket) end)
44 | {:noreply, state, {:continue, :accept}}
45 |
46 | {:error, reason} ->
47 | {:stop, reason}
48 | end
49 | end
50 |
51 | ## Helpers
52 |
53 | defp handle_connection(socket) do
54 | case echo_lines_until_closed(socket) do
55 | :ok -> :ok
56 | {:error, reason} -> Logger.error("Failed to receive data: #{inspect(reason)}")
57 | end
58 |
59 | :gen_tcp.close(socket)
60 | end
61 |
62 | defp echo_lines_until_closed(socket) do
63 | case :gen_tcp.recv(socket, 0, 10_000) do
64 | {:ok, data} ->
65 | case Jason.decode(data) do
66 | {:ok, %{"method" => "isPrime", "number" => number}} when is_number(number) ->
67 | Logger.debug("Received valid request for number: #{number}")
68 | response = %{"method" => "isPrime", "prime" => prime?(number)}
69 | :gen_tcp.send(socket, [Jason.encode!(response), ?\n])
70 | echo_lines_until_closed(socket)
71 |
72 | other ->
73 | Logger.debug("Received invalid request: #{inspect(other)}")
74 | :gen_tcp.send(socket, "malformed request\n")
75 | {:error, :invalid_request}
76 | end
77 |
78 | {:error, :closed} ->
79 | :ok
80 |
81 | {:error, reason} ->
82 | {:error, reason}
83 | end
84 | end
85 |
86 | defp prime?(number) when is_float(number), do: false
87 | defp prime?(number) when number <= 1, do: false
88 | defp prime?(number) when number in [2, 3], do: true
89 |
90 | defp prime?(number) do
91 | not Enum.any?(2..trunc(:math.sqrt(number)), &(rem(number, &1) == 0))
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/lib/protohackers/udp_server.ex:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.UDPServer do
2 | use GenServer
3 |
4 | require Logger
5 |
6 | @spec start_link(keyword()) :: GenServer.on_start()
7 | def start_link(opts) do
8 | GenServer.start_link(__MODULE__, opts)
9 | end
10 |
11 | defstruct [:socket, store: %{"version" => "Protohackers in Elixir 1.0"}]
12 |
13 | @impl true
14 | def init(opts) do
15 | port = Keyword.fetch!(opts, :port)
16 |
17 | address =
18 | case System.fetch_env("FLY_APP_NAME") do
19 | {:ok, _} ->
20 | {:ok, fly_global_ip} = :inet.getaddr(~c"fly-global-services", :inet)
21 | fly_global_ip
22 |
23 | :error ->
24 | {0, 0, 0, 0}
25 | end
26 |
27 | Logger.info("Started server on #{:inet.ntoa(address)}:#{port}")
28 |
29 | case :gen_udp.open(5005, [:binary, active: false, recbuf: 1000, ip: address]) do
30 | {:ok, socket} ->
31 | state = %__MODULE__{socket: socket}
32 | {:ok, state, {:continue, :recv}}
33 |
34 | {:error, reason} ->
35 | {:stop, reason}
36 | end
37 | end
38 |
39 | @impl true
40 | def handle_continue(:recv, %__MODULE__{} = state) do
41 | case :gen_udp.recv(state.socket, 0) do
42 | {:ok, {address, port, packet}} ->
43 | Logger.debug(
44 | "Received UDP packet from #{inspect(address)}:#{inspect(port)}: #{inspect(packet)}"
45 | )
46 |
47 | state =
48 | case String.split(packet, "=", parts: 2) do
49 | # Don't do anything for the "version" key.
50 | ["version", _value] ->
51 | state
52 |
53 | [key, value] ->
54 | Logger.debug("Inserted key #{inspect(key)} with value #{inspect(value)}")
55 | put_in(state.store[key], value)
56 |
57 | [key] ->
58 | Logger.debug("Requested key: #{inspect(key)}")
59 | packet = "#{key}=#{state.store[key]}"
60 | :gen_udp.send(state.socket, address, port, packet)
61 | state
62 | end
63 |
64 | {:noreply, state, {:continue, :recv}}
65 |
66 | {:error, reason} ->
67 | {:stop, reason}
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule ProtohackersFirstDays.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :protohackers_first_days,
7 | version: "0.1.0",
8 | elixir: "~> 1.14",
9 | start_permanent: Mix.env() == :prod,
10 | deps: deps()
11 | ]
12 | end
13 |
14 | # Run "mix help compile.app" to learn about applications.
15 | def application do
16 | [
17 | extra_applications: [:logger],
18 | mod: {Protohackers.Application, []}
19 | ]
20 | end
21 |
22 | # Run "mix help deps" to learn about dependencies.
23 | defp deps do
24 | [
25 | {:jason, "~> 1.4"}
26 | ]
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
3 | }
4 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/rel/env.bat.eex:
--------------------------------------------------------------------------------
1 | @echo off
2 | rem Set the release to load code on demand (interactive) instead of preloading (embedded).
3 | rem set RELEASE_MODE=interactive
4 |
5 | rem Set the release to work across nodes.
6 | rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
7 | rem set RELEASE_DISTRIBUTION=name
8 | rem set RELEASE_NODE=<%= @release.name %>
9 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/rel/env.sh.eex:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ip=$(grep fly-local-6pn /etc/hosts | cut -f 1)
4 | export RELEASE_DISTRIBUTION=name
5 | export RELEASE_NODE=$FLY_APP_NAME@"$ip"
6 | export ELIXIR_ERL_OPTIONS="-proto_dist inet6_tcp"
7 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/rel/remote.vm.args.eex:
--------------------------------------------------------------------------------
1 | ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html
2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here
3 |
4 | ## Increase number of concurrent ports/sockets
5 | ##+Q 65536
6 |
7 | ## Tweak GC to run more often
8 | ##-env ERL_FULLSWEEP_AFTER 10
9 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/rel/vm.args.eex:
--------------------------------------------------------------------------------
1 | ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html
2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here
3 |
4 | ## Increase number of concurrent ports/sockets
5 | ##+Q 65536
6 |
7 | ## Tweak GC to run more often
8 | ##-env ERL_FULLSWEEP_AFTER 10
9 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/test/protohackers/budget_chat_server_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.BudgetChatServerTest do
2 | use ExUnit.Case, async: true
3 |
4 | test "whole flow" do
5 | {:ok, socket1} =
6 | :gen_tcp.connect(~c"localhost", 5004, mode: :binary, active: false, packet: :line)
7 |
8 | {:ok, socket2} =
9 | :gen_tcp.connect(~c"localhost", 5004, mode: :binary, active: false, packet: :line)
10 |
11 | assert {:ok, "What's your username?\n"} = :gen_tcp.recv(socket1, 0, 5_000)
12 | :ok = :gen_tcp.send(socket1, "Sock1\n")
13 | assert {:ok, "* The room contains: \n"} = :gen_tcp.recv(socket1, 0, 5_000)
14 |
15 | assert {:ok, "What's your username?\n"} = :gen_tcp.recv(socket2, 0, 5_000)
16 |
17 | :ok = :gen_tcp.send(socket2, "Sock2\n")
18 | assert {:ok, "* The room contains: Sock1\n"} = :gen_tcp.recv(socket2, 0, 5_000)
19 | assert {:ok, "* Sock2 has entered the chat\n"} = :gen_tcp.recv(socket1, 0, 5_000)
20 |
21 | :ok = :gen_tcp.send(socket1, "Hello world!\n")
22 | assert {:ok, "[Sock1] Hello world!\n"} = :gen_tcp.recv(socket2, 0, 5_000)
23 |
24 | :ok = :gen_tcp.send(socket2, "Hi to you!\n")
25 | assert {:ok, "[Sock2] Hi to you!\n"} = :gen_tcp.recv(socket1, 0, 5_000)
26 |
27 | :gen_tcp.close(socket2)
28 |
29 | assert {:ok, "* Sock2 left\n"} = :gen_tcp.recv(socket1, 0, 5_000)
30 |
31 | {:ok, socket3} =
32 | :gen_tcp.connect(~c"localhost", 5004, mode: :binary, active: false, packet: :line)
33 |
34 | assert {:ok, "What's your username?\n"} = :gen_tcp.recv(socket3, 0, 5_000)
35 | :ok = :gen_tcp.send(socket3, "Sock3\n")
36 | assert {:ok, "* The room contains: Sock1\n"} = :gen_tcp.recv(socket3, 0, 5_000)
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/test/protohackers/echo_server_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.EchoServerTest do
2 | use ExUnit.Case, async: true
3 |
4 | test "echoes anything back" do
5 | {:ok, socket} = :gen_tcp.connect(~c"localhost", 5001, mode: :binary, active: false)
6 | assert :gen_tcp.send(socket, "foo") == :ok
7 | assert :gen_tcp.send(socket, "bar") == :ok
8 | :gen_tcp.shutdown(socket, :write)
9 | assert :gen_tcp.recv(socket, 0, 5000) == {:ok, "foobar"}
10 | end
11 |
12 | @tag :capture_log
13 | test "echo server has a max buffer size" do
14 | {:ok, socket} = :gen_tcp.connect(~c"localhost", 5001, mode: :binary, active: false)
15 | assert :gen_tcp.send(socket, :binary.copy("a", 1024 * 100 + 1)) == :ok
16 | assert :gen_tcp.recv(socket, 0) == {:error, :closed}
17 | end
18 |
19 | test "handles multiple concurrent connections" do
20 | tasks =
21 | for _ <- 1..4 do
22 | Task.async(fn ->
23 | {:ok, socket} = :gen_tcp.connect(~c"localhost", 5001, mode: :binary, active: false)
24 | assert :gen_tcp.send(socket, "foo") == :ok
25 | assert :gen_tcp.send(socket, "bar") == :ok
26 | :gen_tcp.shutdown(socket, :write)
27 | assert :gen_tcp.recv(socket, 0, 5000) == {:ok, "foobar"}
28 | end)
29 | end
30 |
31 | Enum.each(tasks, &Task.await/1)
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/test/protohackers/mitm/boguscoin_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.MIMT.BoguscoinTest do
2 | use ExUnit.Case, async: true
3 |
4 | alias Protohackers.MITM.Boguscoin
5 |
6 | @tonys_address "7YWHMfk9JZe0LM0g1ZauHuiSxhI"
7 |
8 | @sample_addresses [
9 | "7F1u3wSD5RbOHQmupo9nx4TnhQ",
10 | "7iKDZEwPZSqIvDnHvVN2r0hUWXD5rHX",
11 | "7LOrwbDlS8NujgjddyogWgIM93MV5N2VR",
12 | "7adNeSwJkMakpEcln9HEtthSRtxdmEHOT8T"
13 | ]
14 |
15 | describe "rewrite_addresses/1" do
16 | test "doesn't do anything if there are no addresses" do
17 | assert Boguscoin.rewrite_addresses("hello") == "hello"
18 | end
19 |
20 | test "ignores too-long addresses" do
21 | str = "This is too long: 7xtGNgS2V2d32VsCXYpZSdQXOY4Iy7vlVboZ"
22 | assert Boguscoin.rewrite_addresses(str) == str
23 | end
24 |
25 | test "ignores suspicious addresses" do
26 | str = "Not Boguscoin: 7cuyhvvuR2kduZsmZNmBqmUJpZTjMDbhtsD-PI1NrEdUAZ8Ar5NX0DUTdCUEo55V-1234"
27 |
28 | assert Boguscoin.rewrite_addresses(str) == str
29 | end
30 |
31 | test "multiple addresses" do
32 | assert Boguscoin.rewrite_addresses(Enum.join(@sample_addresses, " ")) ==
33 | Enum.join(
34 | List.duplicate(
35 | @tonys_address,
36 | length(@sample_addresses)
37 | ),
38 | " "
39 | )
40 | end
41 |
42 | test "with sample addresses" do
43 | for address <- @sample_addresses do
44 | assert Boguscoin.rewrite_addresses(address) == @tonys_address
45 | assert Boguscoin.rewrite_addresses(address <> " foo") == @tonys_address <> " foo"
46 | assert Boguscoin.rewrite_addresses("foo " <> address) == "foo " <> @tonys_address
47 |
48 | assert Boguscoin.rewrite_addresses("foo " <> address <> " bar") ==
49 | "foo " <> @tonys_address <> " bar"
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/test/protohackers/prices_server/db_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.PricesServer.DBTest do
2 | use ExUnit.Case, async: true
3 |
4 | alias Protohackers.PricesServer.DB
5 |
6 | test "adding elements and getting the average" do
7 | db = DB.new()
8 |
9 | assert DB.query(db, 0, 100) == 0
10 |
11 | db =
12 | db
13 | |> DB.add(1, 10)
14 | |> DB.add(2, 20)
15 | |> DB.add(3, 30)
16 |
17 | assert DB.query(db, 0, 100) == 20
18 | assert DB.query(db, 0, 2) == 15
19 | assert DB.query(db, 2, 3) == 25
20 | assert DB.query(db, 4, 100) == 0
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/test/protohackers/prices_server_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.PricesServerTest do
2 | use ExUnit.Case, async: true
3 |
4 | test "handles queries" do
5 | {:ok, socket} = :gen_tcp.connect(~c"localhost", 5003, mode: :binary, active: false)
6 |
7 | :ok = :gen_tcp.send(socket, <>)
8 | :ok = :gen_tcp.send(socket, <>)
9 | :ok = :gen_tcp.send(socket, <>)
10 |
11 | :ok = :gen_tcp.send(socket, <>)
12 | assert {:ok, <<2::32-signed-big>>} = :gen_tcp.recv(socket, 4, 10_000)
13 | end
14 |
15 | test "handles clients separately" do
16 | {:ok, socket1} = :gen_tcp.connect(~c"localhost", 5003, mode: :binary, active: false)
17 | {:ok, socket2} = :gen_tcp.connect(~c"localhost", 5003, mode: :binary, active: false)
18 |
19 | :ok = :gen_tcp.send(socket1, <>)
20 | :ok = :gen_tcp.send(socket2, <>)
21 |
22 | :ok = :gen_tcp.send(socket1, <>)
23 | assert {:ok, <<1::32-signed-big>>} = :gen_tcp.recv(socket1, 4, 10_000)
24 |
25 | :ok = :gen_tcp.send(socket2, <>)
26 | assert {:ok, <<2::32-signed-big>>} = :gen_tcp.recv(socket2, 4, 10_000)
27 | end
28 |
29 | test "at least five simultaneous clients are supported" do
30 | 0..50
31 | |> Enum.map(fn _ ->
32 | Task.async(fn ->
33 | {:ok, socket} = :gen_tcp.connect(~c"localhost", 5003, mode: :binary, active: false)
34 | :ok = :gen_tcp.send(socket, <>)
35 | :ok = :gen_tcp.send(socket, <>)
36 | :ok = :gen_tcp.send(socket, <>)
37 | :ok = :gen_tcp.send(socket, <>)
38 | :ok = :gen_tcp.send(socket, <>)
39 |
40 | assert :gen_tcp.recv(socket, 0) == {:ok, <<101::32-signed-big>>}
41 |
42 | :ok = :gen_tcp.close(socket)
43 | end)
44 | end)
45 | |> Task.await_many()
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/test/protohackers/prime_server_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.PrimeServerTest do
2 | use ExUnit.Case, async: true
3 |
4 | test "echoes back JSON" do
5 | {:ok, socket} = :gen_tcp.connect(~c"localhost", 5002, mode: :binary, active: false)
6 | :gen_tcp.send(socket, Jason.encode!(%{method: "isPrime", number: 7}) <> "\n")
7 |
8 | assert {:ok, data} = :gen_tcp.recv(socket, 0, 5000)
9 | assert String.ends_with?(data, "\n")
10 | assert Jason.decode!(data) == %{"method" => "isPrime", "prime" => true}
11 |
12 | :gen_tcp.send(socket, Jason.encode!(%{method: "isPrime", number: 6}) <> "\n")
13 |
14 | assert {:ok, data} = :gen_tcp.recv(socket, 0, 5000)
15 | assert String.ends_with?(data, "\n")
16 | assert Jason.decode!(data) == %{"method" => "isPrime", "prime" => false}
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/test/protohackers/udp_server_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.UDPServerTest do
2 | use ExUnit.Case, async: true
3 |
4 | test "insert and retrieve requests" do
5 | {:ok, socket} = :gen_udp.open(0, [:binary, active: false, recbuf: 1000])
6 |
7 | :ok = :gen_udp.send(socket, {127, 0, 0, 1}, 5005, "foo=1")
8 | :ok = :gen_udp.send(socket, {127, 0, 0, 1}, 5005, "foo")
9 | assert {:ok, {_address, _port, "foo=1"}} = :gen_udp.recv(socket, 0)
10 |
11 | :ok = :gen_udp.send(socket, {127, 0, 0, 1}, 5005, "foo=2")
12 | :ok = :gen_udp.send(socket, {127, 0, 0, 1}, 5005, "foo")
13 | assert {:ok, {_address, _port, "foo=2"}} = :gen_udp.recv(socket, 0)
14 | end
15 |
16 | test "version" do
17 | {:ok, socket} = :gen_udp.open(0, [:binary, active: false, recbuf: 1000])
18 |
19 | :ok = :gen_udp.send(socket, {127, 0, 0, 1}, 5005, "version=foo")
20 | :ok = :gen_udp.send(socket, {127, 0, 0, 1}, 5005, "version")
21 |
22 | assert {:ok, {_address, _port, "version=Protohackers in Elixir 1.0"}} =
23 | :gen_udp.recv(socket, 0)
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/apps/protohackers_first_days/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start(assert_receive_timeout: 1_000)
2 |
--------------------------------------------------------------------------------
/apps/speed_daemon/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 | ]
5 |
--------------------------------------------------------------------------------
/apps/speed_daemon/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where third-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | speed_daemon-*.tar
24 |
25 | # Temporary files, for example, from tests.
26 | /tmp/
27 |
--------------------------------------------------------------------------------
/apps/speed_daemon/lib/speed_daemon/acceptor.ex:
--------------------------------------------------------------------------------
1 | defmodule SpeedDaemon.Acceptor do
2 | use Task, restart: :transient
3 |
4 | require Logger
5 |
6 | @spec start_link(keyword()) :: GenServer.on_start()
7 | def start_link(opts) do
8 | Task.start_link(__MODULE__, :run, [Keyword.fetch!(opts, :port)])
9 | end
10 |
11 | @spec run(:inet.port_number()) :: no_return()
12 | def run(port) do
13 | case :gen_tcp.listen(port, [
14 | :binary,
15 | ifaddr: {0, 0, 0, 0},
16 | active: :once,
17 | reuseaddr: true
18 | ]) do
19 | {:ok, listen_socket} ->
20 | Logger.info("Listening on port #{port}")
21 | accept_loop(listen_socket)
22 |
23 | {:error, reason} ->
24 | raise "failed to listen on port #{port}: #{inspect(reason)}"
25 | end
26 | end
27 |
28 | defp accept_loop(listen_socket) do
29 | case :gen_tcp.accept(listen_socket) do
30 | {:ok, socket} ->
31 | {:ok, _} = SpeedDaemon.ConnectionSupervisor.start_child(socket)
32 | accept_loop(listen_socket)
33 |
34 | {:error, reason} ->
35 | raise "failed to accept connection: #{inspect(reason)}"
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/apps/speed_daemon/lib/speed_daemon/application.ex:
--------------------------------------------------------------------------------
1 | defmodule SpeedDaemon.Application do
2 | # See https://hexdocs.pm/elixir/Application.html
3 | # for more information on OTP Applications
4 | @moduledoc false
5 |
6 | use Application
7 |
8 | @impl true
9 | def start(_type, _args) do
10 | port = String.to_integer(System.get_env("TCP_PORT", "5007"))
11 |
12 | children = [
13 | {SpeedDaemon.Supervisor, port: port}
14 | ]
15 |
16 | # See https://hexdocs.pm/elixir/Supervisor.html
17 | # for other strategies and supported options
18 | opts = [strategy: :one_for_one, name: SpeedDaemon.Supervisor]
19 | Supervisor.start_link(children, opts)
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/apps/speed_daemon/lib/speed_daemon/central_ticket_dispatcher.ex:
--------------------------------------------------------------------------------
1 | defmodule SpeedDaemon.CentralTicketDispatcher do
2 | use GenServer
3 |
4 | alias SpeedDaemon.{DispatchersRegistry, Message}
5 |
6 | require Logger
7 |
8 | def start_link([] = _opts) do
9 | GenServer.start_link(__MODULE__, :no_args, name: __MODULE__)
10 | end
11 |
12 | def add_road(road, speed_limit) do
13 | GenServer.cast(__MODULE__, {:add_road, road, speed_limit})
14 | end
15 |
16 | def register_observation(road, location, plate, timestamp) do
17 | GenServer.cast(__MODULE__, {:register_observation, road, location, plate, timestamp})
18 | end
19 |
20 | ## State
21 |
22 | defmodule Road do
23 | defstruct [:id, :speed_limit, observations: %{}, pending_tickets: []]
24 | end
25 |
26 | defstruct roads: %{}, sent_tickets_per_day: []
27 |
28 | ## Callbacks
29 |
30 | @impl true
31 | def init(:no_args) do
32 | {:ok, %__MODULE__{}}
33 | end
34 |
35 | @impl true
36 | def handle_cast(cast, state)
37 |
38 | def handle_cast({:add_road, road_id, speed_limit}, state) do
39 | Logger.debug("Added road #{road_id} with speed limit #{speed_limit}")
40 | new_road = %Road{id: road_id, speed_limit: speed_limit}
41 | state = update_in(state.roads, &Map.put_new(&1, road_id, new_road))
42 | {:noreply, state}
43 | end
44 |
45 | def handle_cast({:register_observation, road_id, location, plate, timestamp}, state) do
46 | state =
47 | update_in(state.roads[road_id].observations[plate], fn observations ->
48 | observations = observations || []
49 | [{timestamp, location}] ++ observations
50 | end)
51 |
52 | road = generate_tickets(state.roads[road_id], plate)
53 |
54 | state = put_in(state.roads[road_id], road)
55 | state = dispatch_tickets_to_available_dispatchers(state, road_id)
56 | {:noreply, state}
57 | end
58 |
59 | @impl true
60 | def handle_info(info, state)
61 |
62 | def handle_info({:register, DispatchersRegistry, road_id, _partition, _value}, state) do
63 | state = dispatch_tickets_to_available_dispatchers(state, road_id)
64 | {:noreply, state}
65 | end
66 |
67 | # We don't need to do anything here.
68 | def handle_info({:unregister, DispatchersRegistry, _dispatcher, _partition}, state) do
69 | {:noreply, state}
70 | end
71 |
72 | ## Helpers
73 |
74 | defp generate_tickets(%Road{} = road, plate) do
75 | observations =
76 | road.observations[plate]
77 | |> Enum.sort_by(fn {timestamp, _location} -> timestamp end)
78 | |> Enum.dedup_by(fn {timestamp, _location} -> timestamp end)
79 |
80 | tickets =
81 | observations
82 | |> Stream.zip(Enum.drop(observations, 1))
83 | |> Enum.flat_map(fn {{ts1, location1}, {ts2, location2}} ->
84 | distance = abs(location1 - location2)
85 | speed_miles_per_hour = round(distance / (ts2 - ts1) * 3600)
86 |
87 | if speed_miles_per_hour > road.speed_limit do
88 | [
89 | %Message.Ticket{
90 | plate: plate,
91 | road: road.id,
92 | mile1: location1,
93 | timestamp1: ts1,
94 | mile2: location2,
95 | timestamp2: ts2,
96 | speed: speed_miles_per_hour * 100
97 | }
98 | ]
99 | else
100 | []
101 | end
102 | end)
103 |
104 | %Road{road | pending_tickets: road.pending_tickets ++ tickets}
105 | end
106 |
107 | defp dispatch_tickets_to_available_dispatchers(state, road_id) do
108 | case Map.fetch(state.roads, road_id) do
109 | {:ok, %Road{} = road} ->
110 | {tickets_left_to_dispatch, sent_tickets_per_day} =
111 | Enum.flat_map_reduce(
112 | state.roads[road_id].pending_tickets,
113 | state.sent_tickets_per_day,
114 | fn ticket, acc ->
115 | case Registry.lookup(DispatchersRegistry, road.id) do
116 | [] ->
117 | Logger.debug("No dispatchers available for road #{ticket.road}, keeping ticket")
118 | {[ticket], acc}
119 |
120 | dispatchers ->
121 | ticket_start_day = floor(ticket.timestamp1 / 86_400)
122 | ticket_end_day = floor(ticket.timestamp2 / 86_400)
123 |
124 | if {ticket_start_day, ticket.plate} in acc or
125 | {ticket_end_day, ticket.plate} in acc do
126 | Logger.debug(
127 | "Not sending ticket because it was already sent for this day: #{inspect(ticket)}"
128 | )
129 |
130 | {[], acc}
131 | else
132 | {pid, _} = Enum.random(dispatchers)
133 | GenServer.cast(pid, {:dispatch_ticket, ticket})
134 |
135 | sent = for day <- ticket_start_day..ticket_end_day, do: {day, ticket.plate}
136 | {[], acc ++ sent}
137 | end
138 | end
139 | end
140 | )
141 |
142 | state = put_in(state.sent_tickets_per_day, sent_tickets_per_day)
143 | state = put_in(state.roads[road_id].pending_tickets, tickets_left_to_dispatch)
144 | state
145 |
146 | :error ->
147 | state
148 | end
149 | end
150 | end
151 |
--------------------------------------------------------------------------------
/apps/speed_daemon/lib/speed_daemon/connection.ex:
--------------------------------------------------------------------------------
1 | defmodule SpeedDaemon.Connection do
2 | use GenServer, restart: :temporary
3 |
4 | alias SpeedDaemon.{CentralTicketDispatcher, DispatchersRegistry, Message}
5 |
6 | require Logger
7 |
8 | def start_link(socket) do
9 | GenServer.start_link(__MODULE__, socket)
10 | end
11 |
12 | defstruct [:socket, :type, :heartbeat_ref, buffer: <<>>]
13 |
14 | @impl true
15 | def init(socket) do
16 | Logger.debug("Client connected")
17 | {:ok, %__MODULE__{socket: socket}}
18 | end
19 |
20 | @impl true
21 | def handle_info(message, state)
22 |
23 | def handle_info({:tcp, socket, data}, %__MODULE__{socket: socket} = state) do
24 | state = update_in(state.buffer, &(&1 <> data))
25 | :ok = :inet.setopts(socket, active: :once)
26 | parse_all_data(state)
27 | end
28 |
29 | def handle_info({:tcp_error, socket, reason}, %__MODULE__{socket: socket} = state) do
30 | Logger.error("Connection closed because of error: #{inspect(reason)}")
31 | {:stop, :normal, state}
32 | end
33 |
34 | def handle_info({:tcp_closed, socket}, %__MODULE__{socket: socket} = state) do
35 | Logger.debug("Connection closed by client")
36 | {:stop, :normal, state}
37 | end
38 |
39 | def handle_info(:send_heartbeat, %__MODULE__{} = state) do
40 | send_message(state, %Message.Heartbeat{})
41 | {:noreply, state}
42 | end
43 |
44 | @impl true
45 | def handle_cast({:dispatch_ticket, ticket}, %__MODULE__{type: %Message.IAmDispatcher{}} = state) do
46 | send_message(state, ticket)
47 | {:noreply, state}
48 | end
49 |
50 | ## Helpers
51 |
52 | defp send_message(%__MODULE__{socket: socket}, message) do
53 | Logger.debug("Sending message: #{inspect(message)}")
54 | :gen_tcp.send(socket, Message.encode(message))
55 | end
56 |
57 | defp parse_all_data(%__MODULE__{} = state) do
58 | case Message.decode(state.buffer) do
59 | {:ok, message, rest} ->
60 | Logger.debug("Received message: #{inspect(message)}")
61 | state = put_in(state.buffer, rest)
62 |
63 | case handle_message(state, message) do
64 | {:ok, state} ->
65 | parse_all_data(state)
66 |
67 | {:error, message} ->
68 | send_message(state, %Message.Error{message: message})
69 | {:stop, :normal, state}
70 | end
71 |
72 | :incomplete ->
73 | {:noreply, state}
74 |
75 | :error ->
76 | send_message(state, %Message.Error{message: "Invalid protocol message"})
77 | {:stop, :normal, state}
78 | end
79 | end
80 |
81 | defp handle_message(
82 | %__MODULE__{type: %Message.IAmCamera{} = camera} = state,
83 | %Message.Plate{} = message
84 | ) do
85 | CentralTicketDispatcher.register_observation(
86 | camera.road,
87 | camera.mile,
88 | message.plate,
89 | message.timestamp
90 | )
91 |
92 | {:ok, state}
93 | end
94 |
95 | defp handle_message(%__MODULE__{type: _other_type}, %Message.Plate{}) do
96 | {:error, "Plate messages are only accepted from cameras"}
97 | end
98 |
99 | defp handle_message(state, %Message.WantHeartbeat{interval: interval}) do
100 | interval_in_ms = interval * 100
101 |
102 | if state.heartbeat_ref do
103 | :timer.cancel(state.heartbeat_ref)
104 | end
105 |
106 | if interval > 0 do
107 | {:ok, heartbeat_ref} = :timer.send_interval(interval_in_ms, :send_heartbeat)
108 | {:ok, %__MODULE__{state | heartbeat_ref: heartbeat_ref}}
109 | else
110 | {:ok, %__MODULE__{state | heartbeat_ref: nil}}
111 | end
112 | end
113 |
114 | defp handle_message(%__MODULE__{type: nil} = state, %Message.IAmCamera{} = message) do
115 | CentralTicketDispatcher.add_road(message.road, message.limit)
116 | Logger.metadata(type: :camera, road: message.road, mile: message.mile)
117 |
118 | {:ok, %__MODULE__{state | type: message}}
119 | end
120 |
121 | defp handle_message(%__MODULE__{type: _other}, %Message.IAmCamera{}) do
122 | {:error, "Already registered as a dispatcher or a camera"}
123 | end
124 |
125 | defp handle_message(%__MODULE__{type: nil} = state, %Message.IAmDispatcher{} = message) do
126 | Enum.each(message.roads, fn road ->
127 | {:ok, _} = Registry.register(DispatchersRegistry, road, :unused_value)
128 | end)
129 |
130 | Logger.metadata(type: :dispatcher)
131 | {:ok, %__MODULE__{state | type: message}}
132 | end
133 |
134 | defp handle_message(%__MODULE__{type: _other}, %Message.IAmDispatcher{}) do
135 | {:error, "Already registered as a dispatcher or a camera"}
136 | end
137 |
138 | defp handle_message(%__MODULE__{}, _message) do
139 | {:error, "Invalid message"}
140 | end
141 | end
142 |
--------------------------------------------------------------------------------
/apps/speed_daemon/lib/speed_daemon/connection_supervisor.ex:
--------------------------------------------------------------------------------
1 | defmodule SpeedDaemon.ConnectionSupervisor do
2 | use DynamicSupervisor
3 |
4 | @spec start_link(keyword()) :: Supervisor.on_start()
5 | def start_link([] = _opts) do
6 | DynamicSupervisor.start_link(__MODULE__, :no_args, name: __MODULE__)
7 | end
8 |
9 | @spec start_child(:gen_tcp.socket()) :: DynamicSupervisor.on_start_child()
10 | def start_child(socket) do
11 | child_spec = {SpeedDaemon.Connection, socket}
12 |
13 | with {:ok, conn} <- DynamicSupervisor.start_child(__MODULE__, child_spec),
14 | :ok <- :gen_tcp.controlling_process(socket, conn) do
15 | {:ok, conn}
16 | end
17 | end
18 |
19 | @impl true
20 | def init(:no_args) do
21 | DynamicSupervisor.init(strategy: :one_for_one, max_children: 1000)
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/apps/speed_daemon/lib/speed_daemon/message.ex:
--------------------------------------------------------------------------------
1 | defmodule SpeedDaemon.Message do
2 | # Client -> server
3 |
4 | defmodule Plate do
5 | defstruct [:plate, :timestamp]
6 | end
7 |
8 | defmodule WantHeartbeat do
9 | defstruct [:interval]
10 | end
11 |
12 | defmodule IAmCamera do
13 | defstruct [:road, :mile, :limit]
14 | end
15 |
16 | defmodule IAmDispatcher do
17 | defstruct [:roads]
18 | end
19 |
20 | # Server -> client
21 |
22 | defmodule Error do
23 | defstruct [:message]
24 | end
25 |
26 | defmodule Ticket do
27 | defstruct [:plate, :road, :mile1, :timestamp1, :mile2, :timestamp2, :speed]
28 | end
29 |
30 | defmodule Heartbeat do
31 | defstruct []
32 | end
33 |
34 | ## Functions
35 |
36 | @type_bytes [0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08]
37 |
38 | ## Decoding
39 |
40 | # Plate
41 | def decode(<<0x20, plate_size::8, plate::binary-size(plate_size), timestamp::32, rest::binary>>) do
42 | message = %Plate{plate: plate, timestamp: timestamp}
43 | {:ok, message, rest}
44 | end
45 |
46 | # WantHeartbeat
47 | def decode(<<0x40, interval::32, rest::binary>>) do
48 | {:ok, %WantHeartbeat{interval: interval}, rest}
49 | end
50 |
51 | # IAmCamera
52 | def decode(<<0x80, road::16, mile::16, limit::16, rest::binary>>) do
53 | {:ok, %IAmCamera{road: road, mile: mile, limit: limit}, rest}
54 | end
55 |
56 | # IAmDispatcher
57 | def decode(<<0x81, numroads::8, roads::size(numroads * 2)-binary, rest::binary>>) do
58 | roads = for <>, do: road
59 | {:ok, %IAmDispatcher{roads: roads}, rest}
60 | end
61 |
62 | # Ticket
63 | def decode(
64 | <<0x21, plate_size::8, plate::binary-size(plate_size), road::16, mile1::16,
65 | timestamp1::32, mile2::16, timestamp2::32, speed::16, rest::binary>>
66 | ) do
67 | message = %Ticket{
68 | plate: plate,
69 | road: road,
70 | mile1: mile1,
71 | timestamp1: timestamp1,
72 | mile2: mile2,
73 | timestamp2: timestamp2,
74 | speed: speed
75 | }
76 |
77 | {:ok, message, rest}
78 | end
79 |
80 | def decode(<<0x41, rest::binary>>) do
81 | {:ok, %Heartbeat{}, rest}
82 | end
83 |
84 | def decode(<<0x10, size::8, message::size(size)-binary, rest::binary>>) do
85 | {:ok, %Error{message: message}, rest}
86 | end
87 |
88 | def decode(<>) when byte in @type_bytes do
89 | :incomplete
90 | end
91 |
92 | def decode(<<_byte, _rest::binary>>) do
93 | :error
94 | end
95 |
96 | def decode(<<>>) do
97 | :incomplete
98 | end
99 |
100 | ## Encoding
101 |
102 | def encode(message)
103 |
104 | def encode(%Error{message: message}) do
105 | <<0x10, byte_size(message)::8-unsigned-big, message::binary>>
106 | end
107 |
108 | def encode(%Plate{} = plate) do
109 | <<0x20, byte_size(plate.plate)::8, plate.plate::binary, plate.timestamp::32>>
110 | end
111 |
112 | def encode(%WantHeartbeat{interval: interval}) do
113 | <<0x40, interval::32>>
114 | end
115 |
116 | def encode(%IAmCamera{road: road, mile: mile, limit: limit}) do
117 | <<0x80, road::16, mile::16, limit::16>>
118 | end
119 |
120 | def encode(%IAmDispatcher{roads: roads}) do
121 | encoded_roads = IO.iodata_to_binary(for road <- roads, do: <>)
122 | <<0x81, length(roads)::8, encoded_roads::binary>>
123 | end
124 |
125 | def encode(%Heartbeat{}) do
126 | <<0x41>>
127 | end
128 |
129 | def encode(%Ticket{} = ticket) do
130 | <<0x21, byte_size(ticket.plate), ticket.plate::binary, ticket.road::16, ticket.mile1::16,
131 | ticket.timestamp1::32, ticket.mile2::16, ticket.timestamp2::32, ticket.speed::16>>
132 | end
133 | end
134 |
--------------------------------------------------------------------------------
/apps/speed_daemon/lib/speed_daemon/supervisor.ex:
--------------------------------------------------------------------------------
1 | defmodule SpeedDaemon.Supervisor do
2 | use Supervisor
3 |
4 | @spec start_link(keyword()) :: Supervisor.on_start()
5 | def start_link(opts) do
6 | Supervisor.start_link(__MODULE__, opts)
7 | end
8 |
9 | @impl true
10 | def init(opts) do
11 | registry_opts = [
12 | name: SpeedDaemon.DispatchersRegistry,
13 | keys: :duplicate,
14 | listeners: [SpeedDaemon.CentralTicketDispatcher]
15 | ]
16 |
17 | children = [
18 | {Registry, registry_opts},
19 | {SpeedDaemon.CentralTicketDispatcher, []},
20 | {SpeedDaemon.ConnectionSupervisor, []},
21 | {SpeedDaemon.Acceptor, opts}
22 | ]
23 |
24 | Supervisor.init(children, strategy: :rest_for_one)
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/apps/speed_daemon/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule SpeedDaemon.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :speed_daemon,
7 | version: "0.1.0",
8 | build_path: "../../_build",
9 | config_path: "../../config/config.exs",
10 | deps_path: "../../deps",
11 | lockfile: "../../mix.lock",
12 | elixir: "~> 1.14",
13 | start_permanent: Mix.env() == :prod,
14 | deps: deps()
15 | ]
16 | end
17 |
18 | # Run "mix help compile.app" to learn about applications.
19 | def application do
20 | [
21 | extra_applications: [:logger],
22 | mod: {SpeedDaemon.Application, []}
23 | ]
24 | end
25 |
26 | # Run "mix help deps" to learn about dependencies.
27 | defp deps do
28 | []
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/apps/speed_daemon/mix.lock:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whatyouhide/protohackers_in_elixir/f73015610479467c0d60fcaeda6da8dbcc4d6538/apps/speed_daemon/mix.lock
--------------------------------------------------------------------------------
/apps/speed_daemon/rel/env.bat.eex:
--------------------------------------------------------------------------------
1 | @echo off
2 | rem Set the release to load code on demand (interactive) instead of preloading (embedded).
3 | rem set RELEASE_MODE=interactive
4 |
5 | rem Set the release to work across nodes.
6 | rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
7 | rem set RELEASE_DISTRIBUTION=name
8 | rem set RELEASE_NODE=<%= @release.name %>
9 |
--------------------------------------------------------------------------------
/apps/speed_daemon/rel/env.sh.eex:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ip=$(grep fly-local-6pn /etc/hosts | cut -f 1)
4 | export RELEASE_DISTRIBUTION=name
5 | export RELEASE_NODE=$FLY_APP_NAME@"$ip"
6 | export ELIXIR_ERL_OPTIONS="-proto_dist inet6_tcp"
7 |
--------------------------------------------------------------------------------
/apps/speed_daemon/rel/remote.vm.args.eex:
--------------------------------------------------------------------------------
1 | ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html
2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here
3 |
4 | ## Increase number of concurrent ports/sockets
5 | ##+Q 65536
6 |
7 | ## Tweak GC to run more often
8 | ##-env ERL_FULLSWEEP_AFTER 10
9 |
--------------------------------------------------------------------------------
/apps/speed_daemon/rel/vm.args.eex:
--------------------------------------------------------------------------------
1 | ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html
2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here
3 |
4 | ## Increase number of concurrent ports/sockets
5 | ##+Q 65536
6 |
7 | ## Tweak GC to run more often
8 | ##-env ERL_FULLSWEEP_AFTER 10
9 |
--------------------------------------------------------------------------------
/apps/speed_daemon/test/speed_daemon/integration_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SpeedDaemon.IntegrationTest do
2 | use ExUnit.Case
3 |
4 | alias SpeedDaemon.Message
5 |
6 | test "ticketing a single car" do
7 | {:ok, camera1} = :gen_tcp.connect(~c"localhost", 5007, [:binary, active: true])
8 | {:ok, camera2} = :gen_tcp.connect(~c"localhost", 5007, [:binary, active: true])
9 | {:ok, dispatcher} = :gen_tcp.connect(~c"localhost", 5007, [:binary, active: true])
10 |
11 | send_message(dispatcher, %Message.IAmDispatcher{roads: [582]})
12 |
13 | send_message(camera1, %Message.IAmCamera{road: 582, mile: 4452, limit: 100})
14 | send_message(camera1, %Message.Plate{plate: "UK43PKD", timestamp: 203_663})
15 |
16 | send_message(camera2, %Message.IAmCamera{road: 582, mile: 4462, limit: 100})
17 | send_message(camera2, %Message.Plate{plate: "UK43PKD", timestamp: 203_963})
18 |
19 | assert_receive {:tcp, ^dispatcher, data}
20 | assert {:ok, message, <<>>} = Message.decode(data)
21 |
22 | assert message == %Message.Ticket{
23 | mile1: 4452,
24 | mile2: 4462,
25 | plate: "UK43PKD",
26 | road: 582,
27 | speed: 12000,
28 | timestamp1: 203_663,
29 | timestamp2: 203_963
30 | }
31 | end
32 |
33 | test "pending tickets get flushed" do
34 | {:ok, camera1} = :gen_tcp.connect(~c"localhost", 5007, [:binary, active: true])
35 | {:ok, camera2} = :gen_tcp.connect(~c"localhost", 5007, [:binary, active: true])
36 | send_message(camera1, %Message.IAmCamera{road: 582, mile: 4452, limit: 100})
37 | send_message(camera2, %Message.IAmCamera{road: 582, mile: 4462, limit: 100})
38 | send_message(camera1, %Message.Plate{plate: "IT43PRC", timestamp: 203_663})
39 | send_message(camera2, %Message.Plate{plate: "IT43PRC", timestamp: 203_963})
40 |
41 | # We now have a tickets on road 582, but no dispatcher for it.
42 |
43 | {:ok, dispatcher} = :gen_tcp.connect(~c"localhost", 5007, [:binary, active: true])
44 | send_message(dispatcher, %Message.IAmDispatcher{roads: [582]})
45 |
46 | assert_receive {:tcp, ^dispatcher, data}
47 | assert {:ok, message, <<>>} = Message.decode(data)
48 |
49 | assert message == %Message.Ticket{
50 | mile1: 4452,
51 | mile2: 4462,
52 | plate: "IT43PRC",
53 | road: 582,
54 | speed: 12000,
55 | timestamp1: 203_663,
56 | timestamp2: 203_963
57 | }
58 | end
59 |
60 | defp send_message(socket, message) do
61 | assert :ok = :gen_tcp.send(socket, Message.encode(message))
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/apps/speed_daemon/test/speed_daemon/message_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SpeedDaemon.MessageTest do
2 | use ExUnit.Case, async: true
3 |
4 | alias SpeedDaemon.Message
5 |
6 | describe "encode/1 + decode/1 for all messages" do
7 | test "Plate" do
8 | message = %Message.Plate{plate: "UK43PKD", timestamp: 203_663}
9 | assert {:ok, ^message, ""} = Message.decode(Message.encode(message))
10 | end
11 |
12 | test "WantHeartbeat" do
13 | message = %Message.WantHeartbeat{interval: 1000}
14 | assert {:ok, ^message, ""} = Message.decode(Message.encode(message))
15 | end
16 |
17 | test "IAmDispatcher" do
18 | message = %Message.IAmDispatcher{roads: [582]}
19 | assert {:ok, ^message, ""} = Message.decode(Message.encode(message))
20 | end
21 |
22 | test "IAmCamera" do
23 | message = %Message.IAmCamera{road: 582, mile: 4452, limit: 100}
24 | assert {:ok, ^message, ""} = Message.decode(Message.encode(message))
25 | end
26 |
27 | test "Error" do
28 | message = %Message.Error{message: "Something went wrong"}
29 | assert {:ok, ^message, ""} = Message.decode(Message.encode(message))
30 | end
31 |
32 | test "Ticket" do
33 | message = %Message.Ticket{
34 | mile1: 4452,
35 | mile2: 4462,
36 | plate: "UK43PKD",
37 | road: 582,
38 | speed: 12000,
39 | timestamp1: 203_663,
40 | timestamp2: 203_963
41 | }
42 |
43 | assert {:ok, ^message, ""} = Message.decode(Message.encode(message))
44 | end
45 |
46 | test "Heartbeat" do
47 | message = %Message.Heartbeat{}
48 | assert {:ok, ^message, ""} = Message.decode(Message.encode(message))
49 | end
50 | end
51 |
52 | describe "decode/1" do
53 | test "returns :incomplete for valid-looking incomplete messages" do
54 | assert Message.decode(<<>>) == :incomplete
55 | assert Message.decode(<<0x20>>) == :incomplete
56 | end
57 |
58 | test "returns :error for invalid messages" do
59 | assert Message.decode(<<0x00>>) == :error
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/apps/speed_daemon/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | log_level =
4 | if config_env() == :test do
5 | :warn
6 | else
7 | :info
8 | end
9 |
10 | config :logger, level: log_level
11 | config :logger, :console, metadata: [:module, :address, :session, :pid]
12 |
--------------------------------------------------------------------------------
/config/runtime.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | case System.fetch_env("LOG_LEVEL") do
4 | {:ok, level} ->
5 | config :logger, level: String.to_existing_atom(level)
6 |
7 | :error ->
8 | :ok
9 | end
10 |
--------------------------------------------------------------------------------
/fly.toml:
--------------------------------------------------------------------------------
1 | # fly.toml file generated for protohackers-in-elixir on 2022-12-30T16:09:40+01:00
2 |
3 | app = "protohackers-in-elixir"
4 | kill_signal = "SIGTERM"
5 | kill_timeout = 5
6 | processes = []
7 |
8 | [env]
9 | LOG_LEVEL = "debug"
10 | TCP_PORT = "5000"
11 | UDP_PORT = "6000"
12 |
13 | [experimental]
14 | allowed_public_ports = []
15 | auto_rollback = true
16 |
17 | [[services]]
18 | internal_port = 5000
19 | protocol = "tcp"
20 |
21 | [[services.ports]]
22 | handlers = []
23 | port = 5000
24 |
25 | [[services]]
26 | internal_port = 6000
27 | protocol = "udp"
28 |
29 | [[services.ports]]
30 | handlers = []
31 | port = 6000
32 |
33 | [services.concurrency]
34 | hard_limit = 250
35 | soft_limit = 200
36 | type = "connections"
37 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Protohackers.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | apps_path: "apps",
7 | version: "0.1.0",
8 | start_permanent: Mix.env() == :prod,
9 | deps: deps(),
10 | releases: [
11 | protohackers: [
12 | applications: [protohackers_first_days: :permanent]
13 | ],
14 | speed_daemon: [
15 | applications: [speed_daemon: :permanent]
16 | ],
17 | line_reversal: [
18 | applications: [line_reversal: :permanent]
19 | ],
20 | isl: [
21 | applications: [isl: :permanent]
22 | ]
23 | ]
24 | ]
25 | end
26 |
27 | defp deps do
28 | []
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
3 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
4 | "thousand_island": {:hex, :thousand_island, "0.6.0", "fb03f42dfc41760127e7c4d5a401fa1980337a22ceafb7d4b5eca918092a4eef", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "79b6a51234a35cdc59308e7286c67d3b20ae2e3d2011554e95d51ba0011c9a33"},
5 | }
6 |
--------------------------------------------------------------------------------