How to get monitor like thing for an `:ssh`-daemon and calm down dialyzer

I’m having a hard time to fix the following dialyzer warnings:

lib/foo.ex:36:pattern_match
The pattern
{:ok, _pid, _ref, _opts}

can never match the type
{:error, _}

issued about a case/2 expression:

case init_daemon(opts) do
  {:error, reason} ->
    {:error, reason}

  {:ok, pid, ref, opts} ->
    {:ok, %{options: opts, deamons: [%{pid: pid, ref: ref, options: opts}]}}
end

Where init_daemon/1 does roughly this and issues a warning for itself because of an opaque type:

lib/foo.ex:161:call_with_opaque
The call :erlang.monitor('process',_pid@1::ssh:daemon_ref()) contains an opaque term in 2nd argument when terms of different types are expected in these positions}.
def init_daemon(_) do # simplified
  daemon = :ssh.daemon(10022, …irrelevant set of options…)

  case daemon do
    {:ok, pid} ->
      ref = Process.monitor(pid)
      {:ok, pid, ref, %{}}

    {:error, reason} ->
      {:error, reason}
  end
end

My current assumption is, that dialyzer assumes, that in init_daemon/1 the :ok branch will never used, as it were violating opaque types, and therefore considers the :ok clause as superfluous in the first case/2 above. As if I temporarily remove the call to Process.monitor/1 and replace it with a static value, dialyzer does not complain anymore.

But how could I fix that? I need a monitor for the daemon process, or at least some monitorlike behaviour.

I already tried to simply ignore the opaqueness error, but it seems as if this really ignores that single error, not those that are dependend on this…

On the other hand side, I’m already checking what the monitor is used for in this code base (some clients code, that I need to create a CI chain for and the client doesn’t even understand the code as the author has left their company and left about zero comments, documentation or anything in the code)

Isn’t the issue that :ssh.daemon does not return a pid? From the documentation it returns a

{ok, daemon_ref()} | {error, atom()}

where daemon_ref is some opaque type. Thus, for dialyzer the call to Process.monitor will fail since it expects as input a pid. One option you could try is to “clean up” the daemon ref in a custom function that you mark as returning a pid.

@spec clean_up(any) :: pid
defp clean_up(daemon_ref), do: daemon_ref

This certainly looks ugly. But in case daemon_ref is not always a pid, this would also allow for some error checking.

A second option I could think about is having a guard on the {:ok, pid} branch and rewriting it as {:ok, pid} when is_pid(pid)

I didn’t test any of this, so this is just wild speculation…

1 Like

Sadly none of this works, as it will only move the opaque-term warning to a different place or even introduces a “has no local return”.

So for now I simply use module attributes to silence the warnings as specific as I can:

  @dialyzer {:no_match, init: 1}
  @dialyzer {:no_opaque, init_daemon: 1}

I think we will keep it like this until we are able to identify why and how the monitor is used.

You can override dialyzer’s inference by specifically @spec ...'ing init_daemon directly.

However the fact that dialyzer says that ssh returns an {:ok, daemon_ref()} does seem to yell at me that Process.monitor will fail. Although yes ssh does return a pid in the current version, since it is opaque you can’t actually rely on that fact.

Maybe dialyzers type inference could be fixed if you instead did something like replacing the ref = Process.monitor(pid) line with something like ref = if(is_pid(pid), do: Process.monitor(pid)) or so?

Just overriding the inferred spec is probably easiest though. :slight_smile: