Skip to content

Commit

Permalink
#148 retry mpd connection in client itself (#179)
Browse files Browse the repository at this point in the history
Co-authored-by: unknown <unknown@bigvault>
  • Loading branch information
unknown321 and unknown authored Nov 19, 2023
1 parent 4e836fa commit 9948c42
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 75 deletions.
190 changes: 122 additions & 68 deletions recipes/mpc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ local lgi = require "lgi"
local GLib = lgi.GLib
local Gio = lgi.Gio

local gears = require "gears"

local mpc = {}

local function parse_password(host)
Expand All @@ -13,42 +15,63 @@ local function parse_password(host)
return string.sub(host, position + 1), string.sub(host, 1, position - 1)
end

function mpc.new(host, port, password, error_handler, ...)
function mpc.new(host, port, password, error_handler, reconnect_interval, ...)
host = host or os.getenv("MPD_HOST") or "localhost"
port = port or os.getenv("MPD_PORT") or 6600
if not password then
host, password = parse_password(host)
end

local self = setmetatable({
_host = host,
_port = port,
_password = password,
_error_handler = error_handler or function() end,
_connected = false,
_try_reconnect = false,
_idle_commands = { ... }
_idle_commands = { ... },
_conn = nil,
_output = nil,
_input = nil,
_reconnect_timer = nil,
_reconnect_interval = reconnect_interval,
_try_reconnect = true
}, { __index = mpc })

self:_connect()
return self
end

function mpc:_error(err)
self._error_handler(err, self)
end

function mpc:_reset()
self._output = nil
self._input = nil
self._reply_handlers = {}
self._pending_reply = {}
self._idle_commands_pending = false
self._idle = false
self._connected = false
self._error_handler(err)
self._try_reconnect = not self._try_reconnect
if self._try_reconnect then
self:_connect()
end

function mpc:_reconnect()
if not self._reconnect_timer then
self:_error("cannot reconnect")
return
end

if not self._try_reconnect then
return
end

self._reconnect_timer:again()
end

function mpc:_connect()
if self._connected then return end
-- Reset all of our state
self._reply_handlers = {}
self._pending_reply = {}
self._idle_commands_pending = false
self._idle = false
self._connected = true
self:_reset()

-- Set up a new connection
local address
Expand All @@ -60,69 +83,94 @@ function mpc:_connect()
address = Gio.NetworkAddress.new(self._host, self._port)
end
local client = Gio.SocketClient()
local conn, err = client:connect(address)

if not conn then
self:_error(err)
return false
end
local conn
if not self._conn then
if not self._reconnect_timer and self._try_reconnect then
-- timer requires positive value
local interval = self._reconnect_interval >= 0 and self._reconnect_interval or 0
self._reconnect_timer = gears.timer.start_new(interval, function()
-- user disabled reconnect
if self._reconnect_interval < 0 then
self._try_reconnect = false
end

self:_reset()
conn, err = client:connect(address)

if not conn then
self:_error(err)
return true
end

self._connected = true

local input, output = conn:get_input_stream(), conn:get_output_stream()
self._conn, self._output, self._input = conn, output, Gio.DataInputStream.new(input)

local input, output = conn:get_input_stream(), conn:get_output_stream()
self._conn, self._output, self._input = conn, output, Gio.DataInputStream.new(input)
-- Read the welcome message
self._input:read_line()

-- Read the welcome message
self._input:read_line()
if self._password and self._password ~= "" then
self:_send("password " .. self._password)
end

if self._password and self._password ~= "" then
self:_send("password " .. self._password)
return false
end)
else
self:_reconnect()
end
end

self._reconnect_timer:connect_signal("stop", function()
self:do_read()
-- To synchronize the state on startup, send the idle commands now. As a
-- side effect, this will enable idle state.
self:_send_idle_commands(true)
end)
end

function mpc:do_read()
-- Set up the reading loop. This will asynchronously read lines by
-- calling itself.
local do_read
do_read = function()
self._input:read_line_async(GLib.PRIORITY_DEFAULT, nil, function(obj, res)
local line, err = obj:read_line_finish(res)
-- Ugly API. On success we get string, length-of-string
-- and on error we get nil, error. Other versions of lgi
-- behave differently.
if line == nil or tostring(line) == "" then
err = "Connection closed"
end
if type(err) ~= "number" then
self._output, self._input = nil, nil
self:_error(err)
else
do_read()
line = tostring(line)
if line == "OK" or line:match("^ACK ") then
local success = line == "OK"
local arg
if success then
arg = self._pending_reply
else
arg = { line }
end
local handler = self._reply_handlers[1]
table.remove(self._reply_handlers, 1)
self._pending_reply = {}
handler(success, arg)
self._input:read_line_async(GLib.PRIORITY_DEFAULT, nil, function(obj, res)
local line, err = obj:read_line_finish(res)
-- Ugly API. On success we get string, length-of-string
-- and on error we get nil, error. Other versions of lgi
-- behave differently.
if line == nil or tostring(line) == "" then
err = "Connection closed"
self:_error(err)
self:_reconnect()
return
end

if type(err) ~= "number" then
self:_error(err)
self:_reconnect()
else
self:do_read()
line = tostring(line)
if line == "OK" or line:match("^ACK ") then
local success = line == "OK"
local arg
if success then
arg = self._pending_reply
else
local _, _, key, value = string.find(line, "([^:]+):%s(.+)")
if key then
self._pending_reply[string.lower(key)] = value
end
arg = { line }
end
local handler = self._reply_handlers[1]
table.remove(self._reply_handlers, 1)
self._pending_reply = {}
handler(success, arg)
else
local _, _, key, value = string.find(line, "([^:]+):%s(.+)")
if key then
self._pending_reply[string.lower(key)] = value
end
end
end)
end
do_read()

-- To synchronize the state on startup, send the idle commands now. As a
-- side effect, this will enable idle state.
self:_send_idle_commands(true)

return self
end
end)
end

function mpc:_send_idle_commands(skip_stop_idle)
Expand All @@ -148,6 +196,7 @@ end

function mpc:_start_idle()
if self._idle then
self:_error("still idle?!")
error("Still idle?!")
end
self:_send("idle", function(success, reply)
Expand All @@ -161,6 +210,7 @@ end

function mpc:_stop_idle()
if not self._idle then
self:_error("Not idle?!")
error("Not idle?!")
end
self._output:write("noidle\n")
Expand All @@ -169,19 +219,22 @@ end

function mpc:_send(command, callback)
if self._idle then
self:_error("Still idle in send()?!")
error("Still idle in send()?!")
end
self._output:write(command .. "\n")
table.insert(self._reply_handlers, callback or function() end)
end

function mpc:send(...)
self:_connect()
if not self._connected then
if not self._conn then
self:_reconnect()
return
end

local args = { ... }
if not self._idle then
self:_error("Something is messed up, we should be idle here...")
error("Something is messed up, we should be idle here...")
end
self:_stop_idle()
Expand All @@ -204,9 +257,10 @@ end
--[[
-- Example on how to use this (standalone)
-- set negative reconnect_interval to disable reconnect in case initial connection failed
local host, port, password = nil, nil, nil
local m = mpc.new(host, port, password, error,
local host, port, password, reconnect_interval = nil, nil, nil, 0
local m = mpc.new(host, port, password, error_handler, reconnect_interval
"status", function(success, status) print("status is", status.state) end)
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function()
Expand Down
12 changes: 5 additions & 7 deletions recipes/mpc.mdwn
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ updates when the current MPD state changes.

local mpc = require("mpc")
local textbox = require("wibox.widget.textbox")
local timer = require("gears.timer")
local mpd_widget = textbox()
local state, title, artist, file = "stop", "", "", ""
local function update_widget()
Expand All @@ -53,15 +52,14 @@ updates when the current MPD state changes.
end
mpd_widget.text = text
end
local connection

local function error_handler(err)
mpd_widget:set_text("Error: " .. tostring(err))
-- Try a reconnect soon-ish
timer.start_new(10, function()
connection:send("ping")
end)
end
connection = mpc.new(nil, nil, nil, error_handler,

local reconnect_interval = 1 -- negative to disable
local connection
connection = mpc.new(nil, nil, nil, error_handler, reconnect_interval,
"status", function(_, result)
state = result.state
end,
Expand Down

0 comments on commit 9948c42

Please sign in to comment.