about summary refs log tree commit homepage
diff options
context:
space:
mode:
-rwxr-xr-xbin/dtas-mpd-emu11
-rw-r--r--lib/dtas/mpd_emu_client.rb108
-rw-r--r--lib/dtas/server_loop.rb55
-rw-r--r--test/test_mpd_emu.rb73
4 files changed, 247 insertions, 0 deletions
diff --git a/bin/dtas-mpd-emu b/bin/dtas-mpd-emu
new file mode 100755
index 0000000..d95d545
--- /dev/null
+++ b/bin/dtas-mpd-emu
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+# Copyright (C) 2015 all contributors <dtas-all@nongnu.org>
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# MPD protocol emulation proxy in front of dtas-player
+require 'socket'
+require 'dtas/mpd_emu_client'
+require 'dtas/server_loop'
+l = TCPServer.new('127.0.0.1', 2100)
+svc = DTAS::ServerLoop.new([l], DTAS::MpdEmuClient)
+svc.run_forever
diff --git a/lib/dtas/mpd_emu_client.rb b/lib/dtas/mpd_emu_client.rb
new file mode 100644
index 0000000..01258d1
--- /dev/null
+++ b/lib/dtas/mpd_emu_client.rb
@@ -0,0 +1,108 @@
+# Copyright (C) 2015 all contributors <dtas-all@nongnu.org>
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# emulate the MPD protocol
+require_relative '../dtas'
+require 'shellwords'
+
+class DTAS::MpdEmuClient # :nodoc:
+  attr_reader :to_io
+
+  # protocol version we support
+  SERVER = 'MPD 0.13.0'
+  MAX_RBUF = 8192
+  ACK = {
+    ERROR_NOT_LIST: 1,
+    ERROR_ARG: 2,
+    ERROR_PASSWORD: 3,
+    ERROR_PERMISSION: 4,
+    ERROR_UNKNOWN: 5,
+    ERROR_NO_EXIST: 50,
+    ERROR_PLAYLIST_MAX: 51,
+    ERROR_SYSTEM: 52,
+    ERROR_PLAYLIST_LOAD: 53,
+    ERROR_UPDATE_ALREADY: 54,
+    ERROR_PLAYER_SYNC: 55,
+    ERROR_EXIST: 56,
+  }
+
+  def initialize(io)
+    @to_io = io
+    @rbuf = ''.b
+    @wbuf = nil
+    @cmd_listnum = 0
+    out("OK #{SERVER}\n")
+  end
+
+  def dispatch_loop(rbuf)
+    while rbuf.sub!(/\A([^\r\n]+)\r?\n/n, '')
+      rv = dispatch(Shellwords.split($1))
+      next if rv == true
+      return rv
+    end
+    rbuf.size >= MAX_RBUF ? nil : :wait_readable
+  end
+
+  def dispatch(argv)
+    cmd = argv.shift or return err(:ERROR_UNKNOWN)
+    cmd = "mpdcmd_#{cmd}"
+    respond_to?(cmd) ? __send__(cmd, argv) : err(:ERROR_UNKNOWN)
+  end
+
+  def err(sym)
+    "[#{ACK[sym]}@#@cmd_listnum {}"
+  end
+
+  def mpdcmd_ping(argv)
+    out("OK\n")
+  end
+
+  # returns true on complete, :wait_writable when blocked, or nil on error
+  def out(buf)
+    buf = buf.b
+    if @wbuf
+      @wbuf << buf
+      :wait_writable
+    else
+      tot = buf.size
+      case rv = @to_io.write_nonblock(buf, exception: false)
+      when Integer
+        return true if rv == tot
+        buf.slice!(0, rv).clear
+        tot -= rv
+      when :wait_writable
+        @wbuf = buf
+        return rv
+      end while tot > 0
+      true # all done
+    end
+  rescue
+    nil # signal EOF up the chain
+  end
+
+  def dispatch_rd(buf)
+    case rv = @to_io.read_nonblock(MAX_RBUF, buf, exception: false)
+    when String then dispatch_loop(@rbuf << rv)
+    when :wait_readable, nil then rv
+    end
+  rescue
+    nil
+  end
+
+  def dispatch_wr
+    tot = @wbuf.size
+    case rv = @to_io.write_nonblock(@wbuf, exception: false)
+    when Integer
+      @wbuf.slice!(0, rv).clear
+      tot -= rv
+      return :wait_readable if tot == 0
+    when :wait_writable then return rv
+    end while true
+  rescue
+    nil
+  end
+
+  def hash
+    @to_io.fileno
+  end
+end
diff --git a/lib/dtas/server_loop.rb b/lib/dtas/server_loop.rb
new file mode 100644
index 0000000..f0936aa
--- /dev/null
+++ b/lib/dtas/server_loop.rb
@@ -0,0 +1,55 @@
+# Copyright (C) 2015 all contributors <dtas-all@nongnu.org>
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+
+require_relative '../dtas'
+
+# Used for mpd emulation currently, but dtas-player might use this eventually
+class DTAS::ServerLoop
+  def initialize(listeners, client_class)
+    @rd = {}
+    @wr = {}
+    @rbuf = ''
+    @client_class = client_class
+    listeners.each { |l| @rd[l] = true }
+  end
+
+  def run_forever
+    begin
+      r = IO.select(@rd.keys, @wr.keys) or next
+      r[0].each { |rd| do_read(rd) }
+      r[1].each { |wr| do_write(wr) }
+    end while true
+  end
+
+  def do_write(wr)
+    case wr.dispatch_wr
+    when :wait_readable
+      @wr.delete(wr)
+      @rd[wr] = true
+    when nil
+      @wr.delete(wr)
+      wr.to_io.close
+    # when :wait_writable # do nothing
+    end
+  end
+
+  def do_read(rd)
+    case rd
+    when @client_class
+      case rd.dispatch_rd(@rbuf)
+      when :wait_writable
+        @rd.delete(rd)
+        @wr[rd] = true
+      when nil
+        @rd.delete(rd)
+        rd.to_io.close
+      # when :wait_readable : do nothing
+      end
+    else
+      case io = rd.accept_nonblock(exception: false)
+      when :wait_readable then break
+      when IO then @rd[@client_class.new(io)] = true
+      end while true
+    end
+  end
+end
diff --git a/test/test_mpd_emu.rb b/test/test_mpd_emu.rb
new file mode 100644
index 0000000..9c73ae9
--- /dev/null
+++ b/test/test_mpd_emu.rb
@@ -0,0 +1,73 @@
+# Copyright (C) 2013-2015 all contributors <dtas-all@nongnu.org>
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative 'helper'
+require 'socket'
+require 'dtas/mpd_emu_client'
+require 'dtas/server_loop'
+
+class TestMlib < Testcase
+  class Quit
+    attr_reader :to_io
+
+    def initialize
+      @to_io, @w = IO.pipe
+    end
+
+    def quit!
+      @w.close
+    end
+
+    def accept_nonblock(*args)
+      Thread.exit
+    end
+  end
+
+  def setup
+    @host = '127.0.0.1'
+    @l = TCPServer.new(@host, 0)
+    @port = @l.addr[1]
+    @quit = Quit.new
+    @klass = Class.new(DTAS::MpdEmuClient)
+    @svc = DTAS::ServerLoop.new([@l, @quit], @klass)
+    @th = Thread.new { @svc.run_forever }
+    @c = TCPSocket.new(@host, @port)
+    assert_match %r{\AOK.*MPD.*\n\z}, @c.gets
+  end
+
+  def teardown
+    @quit.quit!
+    @th.join
+    @quit.to_io.close
+    @l.close
+    @c.close unless @c.closed?
+  end
+
+  def test_ping
+    @c.write "ping\n"
+    assert_equal "OK\n", @c.gets
+    assert_nil IO.select([@c], nil, nil, 0)
+  end
+
+  # to ensure output buffering works:
+  module BigOutput
+    WAKE = IO.pipe
+    NR = 20000
+    OMG = ('OMG! ' * 99) << "OMG!\n"
+    def mpdcmd_big_output(*_)
+      rv = true
+      NR.times { rv = out(OMG) }
+      # tell the tester we're done writing to our buffer:
+      WAKE[1].write(rv == :wait_writable ? '.' : 'F')
+      rv
+    end
+  end
+
+  def test_big_output
+    @klass.__send__(:include, BigOutput)
+    @c.write "big_output\n"
+    assert_equal '.', BigOutput::WAKE[0].read(1), 'server blocked on write'
+    BigOutput::NR.times do
+      assert_equal BigOutput::OMG, @c.gets
+    end
+  end
+end