Skip to content

Commit f038748

Browse files
committed
Rewrite z3 executor using ex_cmd since porcelain is causing problems on Windows
1 parent 49a0e36 commit f038748

File tree

5 files changed

+50
-202
lines changed

5 files changed

+50
-202
lines changed

lib/ex_smt/solver.ex

Lines changed: 45 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -1,224 +1,70 @@
11
defmodule ExSMT.Solver do
2-
require Logger
2+
use GenServer
33

44
def start_link(init_data) do
55
# IO.puts("Starting z3 solver for #{inspect(Keyword.get(init_data, :room_code))} #{inspect(Keyword.get(init_data, :ruleset))}")
6-
state = %{
7-
room_code: Keyword.get(init_data, :room_code),
8-
ruleset: Keyword.get(init_data, :ruleset)
9-
}
10-
GenServer.start_link(__MODULE__, state, name: Keyword.get(init_data, :name))
6+
GenServer.start_link(__MODULE__, [], name: Keyword.get(init_data, :name))
117
end
128

13-
# def stop do
14-
# case GenServer.whereis(__MODULE__) do
15-
# pid when is_pid(pid) ->
16-
# GenServer.call(pid, :graceful_stop, :infinity)
17-
# :ok = Supervisor.terminate_child(ExSMT.Supervisor, __MODULE__)
18-
# true
19-
# nil ->
20-
# false
21-
# end
22-
# end
23-
24-
# def restart do
25-
# case GenServer.whereis(__MODULE__) do
26-
# nil ->
27-
# Supervisor.restart_child(ExSMT.Supervisor, __MODULE__)
28-
# pid when is_pid(pid) ->
29-
# GenServer.call(pid, :graceful_stop, :infinity)
30-
# end
31-
# end
32-
33-
# def alive? do
34-
# case GenServer.whereis(__MODULE__) do
35-
# nil -> false
36-
# pid when is_pid(pid) -> GenServer.call(__MODULE__, :alive?, 5000)
37-
# end
38-
# end
39-
40-
# def query(iodata, reset \\ true) do
41-
# case GenServer.whereis(__MODULE__) do
42-
# nil -> {:error, :not_running}
43-
# pid when is_pid(pid) ->
44-
# {:ok, response_lns} = GenServer.call(__MODULE__, {:query, iodata, reset}, 5000)
45-
# ExSMT.Solver.ResponseParser.parse(response_lns)
46-
# end
47-
# end
48-
49-
# def query_continue(iodata) do
50-
# query(iodata, false)
51-
# end
52-
53-
use GenServer
54-
55-
defstruct [
56-
mode: :not_running,
57-
process: nil,
58-
launch_cfg: %{},
59-
queries: :queue.new(),
60-
reply: nil
61-
]
62-
639
def init(_state) do
64-
opts = Map.new(Application.get_env(:ex_smt, :z3, []))
65-
66-
z3_path = case Map.fetch(opts, :path) do
67-
{:ok, path} ->
68-
path = Path.expand(path)
69-
cond do
70-
File.dir?(path) ->
71-
{:prefix, path}
72-
File.regular?(path) ->
73-
{:binary, path}
74-
end
75-
:error ->
76-
case :os.find_executable(~c"z3") do
77-
chars when is_list(chars) ->
78-
{:binary, IO.iodata_to_binary(chars)}
79-
false ->
80-
{:prefix, Path.join([:code.priv_dir(:ex_smt), "z3"])}
81-
end
82-
end
83-
84-
z3_bin_path = case z3_path do
85-
{:prefix, prefix} ->
86-
Path.join([prefix, "bin", "z3"])
87-
{:binary, bin_path} ->
88-
case File.read_link(bin_path) do
89-
{:ok, real_path} -> Path.expand(real_path, Path.dirname(bin_path))
90-
{:error, :einval} -> Path.expand(bin_path)
91-
end
92-
end
93-
94-
# make sure that this genserver calls terminate() when killed
95-
Process.flag(:trap_exit, true)
96-
97-
if File.regular?(z3_bin_path) do
98-
launch_cfg = %{
99-
binary: z3_bin_path,
100-
}
101-
102-
{:ok, %__MODULE__{launch_cfg: launch_cfg}, {:continue, :first_start}}
103-
else
104-
:ignore
105-
end
106-
end
107-
108-
def handle_continue(:first_start, %{mode: :not_running} = state0) do
109-
state1 = do_start_node(state0)
110-
{:noreply, state1}
111-
end
112-
113-
def handle_continue(:restart_after_error, %{mode: :not_running} = state0) do
114-
state1 = do_start_node(state0)
115-
{:noreply, state1}
116-
end
117-
118-
def handle_call(:alive?, _from, %{mode: :not_running} = state) do
119-
{:reply, false, state}
120-
end
121-
def handle_call(:alive?, _from, %{mode: :running} = state) do
122-
{:reply, true, state}
123-
end
10+
{:ok, z3} = ExCmd.Process.start_link(~w(z3 -smt2 -in))
12411

125-
def handle_call({:query, _, _}, _from, %{mode: :not_runinng} = state) do
126-
{:reply, {:error, :not_running}, state}
127-
end
128-
def handle_call({:query, iodata, reset}, from, %{mode: :running, process: proc, queries: q} = state) do
129-
script = [
130-
if reset do "(reset)\n" else "" end,
131-
iodata, "\n",
132-
"(echo \"__EOT__\")\n"
133-
]
134-
# Logger.debug(["writing:\n", script])
135-
Porcelain.Process.send_input(proc, script)
136-
{:noreply, %{state | queries: :queue.in(from, q)}}
137-
end
12+
# ex_cmd doesn't want you to trap exit, so don't do that
13+
# unless you are debugging
14+
# Process.flag(:trap_exit, true)
13815

139-
def handle_call(:graceful_stop, _from, state) do
140-
{:stop, :normal, :ok, state}
16+
{:ok, %{process: z3}}
14117
end
14218

143-
def handle_info({_from, :data, :out, iodata}, %{reply: nil, queries: q} = state) do
144-
case :queue.out(q) do
145-
{:empty, _} ->
146-
log_ln = IO.iodata_to_binary(iodata)
147-
Logger.info(IO.ANSI.format([:blue, "z3: ", :reset, inspect(log_ln)]))
148-
{:noreply, state}
149-
{{:value, waiter}, new_q} ->
150-
reply = handle_reply_chunk(iodata, {waiter, []})
151-
{:noreply, %{state | queries: new_q, reply: reply}}
152-
end
153-
end
154-
def handle_info({_from, :data, :out, iodata}, %{reply: reply0} = state) do
155-
reply1 = handle_reply_chunk(iodata, reply0)
156-
{:noreply, %{state | reply: reply1}}
19+
# this is the main thing you call
20+
def handle_call({:query, query, reset?}, _from, %{process: z3} = state) do
21+
msg = mk_msg(query, reset?, true)
22+
# |> IO.inspect(label: "call")
23+
ExCmd.Process.write(z3, msg)
24+
{:ok, out} = poll_z3(z3, "")
25+
ret = out
26+
|> String.split("\n", trim: true)
27+
{:reply, {:ok, ret}, state}
15728
end
15829

159-
def handle_info({_from, :data, :err, iodata}, state) do
160-
log_ln = IO.iodata_to_binary(iodata) |> String.trim()
161-
Logger.error(IO.ANSI.format([:red, "z3: ", :reset, log_ln]))
30+
# this is for when you need no reply
31+
def handle_cast({:query, query, reset?}, %{process: z3} = state) do
32+
msg = mk_msg(query, reset?, false)
33+
# |> IO.inspect(label: "cast")
34+
ExCmd.Process.write(z3, msg)
16235
{:noreply, state}
16336
end
16437

165-
def handle_info({_from, :result, %Porcelain.Result{status: 0}}, %{mode: :running} = state), do:
166-
{:noreply, %{state | mode: :not_running, process: nil}}
167-
def handle_info({_from, :result, %Porcelain.Result{status: status}}, %{mode: :running} = state) do
168-
Logger.error("z3 terminated with exit-code #{status}")
169-
{:noreply, %{state | mode: :not_running, process: nil}, {:continue, :restart_after_error}}
170-
end
171-
172-
def handle_info({_from, :result, _result}, %{mode: :not_running} = state) do
38+
# this is called exclusively when z3 crashes, but requires trapping exits
39+
def handle_info({:EXIT, _pid, reason}, state) do
40+
IO.inspect(reason, label: "Z3 crashed with reason")
17341
{:noreply, state}
17442
end
17543

176-
def handle_info(msg, state) do
177-
Logger.info("z3: got other msg: #{inspect msg}")
178-
{:noreply, state}
179-
end
180-
181-
def terminate(_reason, %{mode: :not_running}), do:
182-
:ok
183-
def terminate(_reason, %{mode: :running, process: proc}) do
184-
Porcelain.Process.stop(proc)
185-
end
186-
187-
defp do_start_node(%{mode: :not_running, launch_cfg: launch_cfg} = state0) do
188-
proc = Porcelain.spawn(
189-
launch_cfg.binary,
190-
["-smt2", "-in"],
191-
in: :receive,
192-
out: {:send, self()},
193-
err: {:send, self()},
194-
result: :keep
195-
)
44+
@eot "__EOT__"
45+
def mk_msg(query, reset?, eot?), do: [
46+
if reset? do "(reset)\n" else "" end,
47+
query,
48+
"\n",
49+
if eot? do "(echo \"#{@eot}\")\n" else "" end
50+
]
19651

197-
%{state0 | mode: :running, process: proc}
52+
def poll_z3(z3, acc) do
53+
case ExCmd.Process.read(z3) do
54+
{:ok, output} ->
55+
acc = String.trim_trailing(acc <> "\n" <> output) # support \r\n and \n
56+
if String.ends_with?(acc, @eot) do
57+
{:ok, String.slice(acc, 0, byte_size(acc) - byte_size(@eot))}
58+
else
59+
poll_z3(z3, acc)
60+
end
61+
err -> {:error, err}
62+
end
19863
end
19964

200-
defp handle_reply_chunk(iodata, {waiter, prev_lns}) do
201-
{:ok, reply_lns} =
202-
IO.iodata_to_binary(iodata)
203-
|> StringIO.open(fn io ->
204-
IO.stream(io, :line)
205-
|> Stream.filter(&not(match?("\n", &1)))
206-
|> Enum.to_list()
207-
end)
208-
209-
handle_reply_lines(reply_lns, prev_lns, waiter)
65+
def terminate(_reason, %{process: z3}) do
66+
ExCmd.Process.close_stdin(z3)
67+
ExCmd.Process.await_exit(z3)
21068
end
21169

212-
defp handle_reply_lines([], acc, waiter) do
213-
{waiter, acc}
214-
end
215-
defp handle_reply_lines(["__EOT__\n"], acc, waiter) do
216-
response_lns = Enum.reverse(acc)
217-
# Logger.debug(["read back:\n", response_lns])
218-
GenServer.reply(waiter, {:ok, response_lns})
219-
nil
220-
end
221-
defp handle_reply_lines([ln | lns], acc, waiter) do
222-
handle_reply_lines(lns, [ln | acc], waiter)
223-
end
22470
end

lib/ex_smt/solver/response_parser.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ defmodule ExSMT.Solver.ResponseParser do
9595
sexpr,
9696
])
9797

98-
newline = ignore(ascii_char([?\n]))
98+
newline = optional(ignore(ascii_char([?\n])))
9999

100100
defparsec :response, repeat(
101101
optional(flexible_space)

lib/riichi_advanced/game/smt.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,7 @@ defmodule RiichiAdvanced.SMT do
779779
IO.puts(smt)
780780
# IO.inspect(encoding)
781781
end
782-
{:ok, _response} = GenServer.call(solver_pid, {:query, smt, true}, 60000)
782+
GenServer.cast(solver_pid, {:query, smt, true})
783783

784784
# stream responses
785785
obtain_all_solutions(solver_pid, encoding, encoding_r, joker_ixs, hand ++ call_tiles, tile_behavior, System.system_time(:millisecond))

mix.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ defmodule RiichiAdvanced.MixProject do
5858
{:hackney, "~> 1.9"},
5959
{:decimal, "~> 2.0"},
6060
{:nimble_parsec, "~> 1.4.0"},
61-
{:porcelain, github: "walkr/porcelain"},
61+
{:porcelain, github: "walkr/porcelain"}, # now unused
6262
{:temp, "~> 0.4"},
6363
{:diff_match_patch, github: "pzingg/diff_match_patch"},
6464
{:diffy, "~> 1.1"},
@@ -69,6 +69,7 @@ defmodule RiichiAdvanced.MixProject do
6969
{:plug_attack, "~> 0.4.2"},
7070
{:sobelow, "~> 0.13", only: [:dev, :test], runtime: false},
7171
{:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
72+
{:ex_cmd, "~> 0.18.0"},
7273
]
7374
end
7475

mix.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
1212
"ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
1313
"esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"},
14+
"ex_cmd": {:hex, :ex_cmd, "0.18.0", "791b42b864c099b67254ca909243e2f60a6e091c802f608d4ae2e3da27b0ade9", [:mix], [], "hexpm", "fd4ec30607a7c789cd53b5fb09189bd0a4922eae5b4fadf70176c19bcd19fb76"},
1415
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
1516
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
1617
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},

0 commit comments

Comments
 (0)