|
1 | 1 | defmodule ExSMT.Solver do |
2 | | - require Logger |
| 2 | + use GenServer |
3 | 3 |
|
4 | 4 | def start_link(init_data) do |
5 | 5 | # 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)) |
11 | 7 | end |
12 | 8 |
|
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 | | - |
63 | 9 | 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)) |
124 | 11 |
|
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) |
138 | 15 |
|
139 | | - def handle_call(:graceful_stop, _from, state) do |
140 | | - {:stop, :normal, :ok, state} |
| 16 | + {:ok, %{process: z3}} |
141 | 17 | end |
142 | 18 |
|
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} |
157 | 28 | end |
158 | 29 |
|
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) |
162 | 35 | {:noreply, state} |
163 | 36 | end |
164 | 37 |
|
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") |
173 | 41 | {:noreply, state} |
174 | 42 | end |
175 | 43 |
|
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 | + ] |
196 | 51 |
|
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 |
198 | 63 | end |
199 | 64 |
|
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(¬(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) |
210 | 68 | end |
211 | 69 |
|
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 |
224 | 70 | end |
0 commit comments