dtas 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +9 -0
  4. data/.rsync_doc +3 -0
  5. data/COPYING +674 -0
  6. data/Documentation/.gitignore +3 -0
  7. data/Documentation/GNUmakefile +46 -0
  8. data/Documentation/dtas-console.txt +42 -0
  9. data/Documentation/dtas-ctl.txt +64 -0
  10. data/Documentation/dtas-cueedit.txt +24 -0
  11. data/Documentation/dtas-enq.txt +29 -0
  12. data/Documentation/dtas-msinkctl.txt +45 -0
  13. data/Documentation/dtas-player.txt +110 -0
  14. data/Documentation/dtas-player_effects.txt +45 -0
  15. data/Documentation/dtas-player_protocol.txt +181 -0
  16. data/Documentation/dtas-sinkedit.txt +41 -0
  17. data/Documentation/dtas-sourceedit.txt +33 -0
  18. data/Documentation/dtas-xdelay.txt +57 -0
  19. data/Documentation/troubleshooting.txt +13 -0
  20. data/GIT-VERSION-GEN +30 -0
  21. data/GNUmakefile +9 -0
  22. data/HACKING +12 -0
  23. data/INSTALL +53 -0
  24. data/README +103 -0
  25. data/Rakefile +97 -0
  26. data/TODO +4 -0
  27. data/bin/dtas-console +160 -0
  28. data/bin/dtas-ctl +10 -0
  29. data/bin/dtas-cueedit +78 -0
  30. data/bin/dtas-enq +13 -0
  31. data/bin/dtas-msinkctl +51 -0
  32. data/bin/dtas-player +34 -0
  33. data/bin/dtas-sinkedit +58 -0
  34. data/bin/dtas-sourceedit +48 -0
  35. data/bin/dtas-xdelay +85 -0
  36. data/dtas-linux.gemspec +18 -0
  37. data/dtas-mpris.gemspec +16 -0
  38. data/examples/dtas_state.yml +18 -0
  39. data/lib/dtas.rb +7 -0
  40. data/lib/dtas/buffer.rb +90 -0
  41. data/lib/dtas/buffer/read_write.rb +102 -0
  42. data/lib/dtas/buffer/splice.rb +142 -0
  43. data/lib/dtas/command.rb +43 -0
  44. data/lib/dtas/compat_onenine.rb +18 -0
  45. data/lib/dtas/disclaimer.rb +18 -0
  46. data/lib/dtas/format.rb +151 -0
  47. data/lib/dtas/pipe.rb +39 -0
  48. data/lib/dtas/player.rb +393 -0
  49. data/lib/dtas/player/client_handler.rb +463 -0
  50. data/lib/dtas/process.rb +87 -0
  51. data/lib/dtas/replaygain.rb +41 -0
  52. data/lib/dtas/rg_state.rb +99 -0
  53. data/lib/dtas/serialize.rb +9 -0
  54. data/lib/dtas/sigevent.rb +10 -0
  55. data/lib/dtas/sigevent/efd.rb +20 -0
  56. data/lib/dtas/sigevent/pipe.rb +28 -0
  57. data/lib/dtas/sink.rb +121 -0
  58. data/lib/dtas/source.rb +147 -0
  59. data/lib/dtas/source/command.rb +40 -0
  60. data/lib/dtas/source/common.rb +14 -0
  61. data/lib/dtas/source/mp3.rb +37 -0
  62. data/lib/dtas/state_file.rb +33 -0
  63. data/lib/dtas/unix_accepted.rb +76 -0
  64. data/lib/dtas/unix_client.rb +51 -0
  65. data/lib/dtas/unix_server.rb +110 -0
  66. data/lib/dtas/util.rb +15 -0
  67. data/lib/dtas/writable_iter.rb +22 -0
  68. data/perl/dtas-graph +129 -0
  69. data/pkg.mk +26 -0
  70. data/setup.rb +1586 -0
  71. data/test/covshow.rb +30 -0
  72. data/test/helper.rb +76 -0
  73. data/test/player_integration.rb +121 -0
  74. data/test/test_buffer.rb +216 -0
  75. data/test/test_format.rb +61 -0
  76. data/test/test_format_change.rb +49 -0
  77. data/test/test_player.rb +47 -0
  78. data/test/test_player_client_handler.rb +86 -0
  79. data/test/test_player_integration.rb +220 -0
  80. data/test/test_rg_integration.rb +117 -0
  81. data/test/test_rg_state.rb +32 -0
  82. data/test/test_sink.rb +32 -0
  83. data/test/test_sink_tee_integration.rb +34 -0
  84. data/test/test_source.rb +102 -0
  85. data/test/test_unixserver.rb +66 -0
  86. data/test/test_util.rb +15 -0
  87. metadata +208 -0
@@ -0,0 +1,40 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require_relative '../../dtas'
5
+ require_relative '../source'
6
+ require_relative '../command'
7
+ require_relative '../serialize'
8
+
9
+ class DTAS::Source::Command # :nodoc:
10
+ require_relative '../source/common'
11
+
12
+ include DTAS::Command
13
+ include DTAS::Process
14
+ include DTAS::Source::Common
15
+ include DTAS::Serialize
16
+
17
+ SIVS = %w(command env)
18
+
19
+ def initialize(command)
20
+ command_init(command: command)
21
+ end
22
+
23
+ def source_dup
24
+ rv = self.class.new
25
+ SIVS.each { |iv| rv.__send__("#{iv}=", self.__send__(iv)) }
26
+ rv
27
+ end
28
+
29
+ def to_hash
30
+ ivars_to_hash(SIVS)
31
+ end
32
+
33
+ alias to_hsh to_hash
34
+
35
+ def spawn(format, rg_state, opts)
36
+ raise "BUG: #{self.inspect}#spawn called twice" if @to_io
37
+ e = format.to_env
38
+ @pid = dtas_spawn(e.merge!(@env), command_string, opts)
39
+ end
40
+ end
@@ -0,0 +1,14 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ module DTAS::Source::Common # :nodoc:
5
+ attr_reader :dst_zero_byte
6
+ attr_reader :dst
7
+ attr_accessor :requeued
8
+
9
+ def dst_assoc(buf)
10
+ @dst = buf
11
+ @dst_zero_byte = buf.bytes_xfer + buf.inflight
12
+ @requeued = false
13
+ end
14
+ end
@@ -0,0 +1,37 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require_relative '../process'
5
+
6
+ module DTAS::Source::Mp3 # :nodoc:
7
+ include DTAS::Process
8
+ # we use dBFS = 1.0 as scale (not 32768)
9
+ def __mp3gain_peak(str)
10
+ sprintf("%0.8g", str.to_f / 32768.0)
11
+ end
12
+
13
+ # massage mp3gain(1) output
14
+ def mp3gain_comments
15
+ tmp = {}
16
+ case @infile
17
+ when String
18
+ @infile =~ /\.mp[g23]\z/i or return
19
+ qx(%W(mp3gain -s c #@infile)).split(/\n/).each do |line|
20
+ case line
21
+ when /^Recommended "(Track|Album)" dB change:\s*(\S+)/
22
+ tmp["REPLAYGAIN_#{$1.upcase}_GAIN"] = $2
23
+ when /^Max PCM sample at current gain: (\S+)/
24
+ tmp["REPLAYGAIN_TRACK_PEAK"] = __mp3gain_peak($1)
25
+ when /^Max Album PCM sample at current gain: (\S+)/
26
+ tmp["REPLAYGAIN_ALBUM_PEAK"] = __mp3gain_peak($1)
27
+ end
28
+ end
29
+ tmp
30
+ else
31
+ raise TypeError, "unsupported type: #{@infile.inspect}"
32
+ end
33
+ rescue => e
34
+ $DEBUG and
35
+ warn("mp3gain(#{@infile.inspect}) failed: #{e.message} (#{e.class})")
36
+ end
37
+ end
@@ -0,0 +1,33 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require 'yaml'
5
+ require 'tempfile'
6
+ class DTAS::StateFile # :nodoc:
7
+ def initialize(path, do_fsync = false)
8
+ @path = path
9
+ @do_fsync = do_fsync
10
+ end
11
+
12
+ def tryload
13
+ YAML.load(IO.binread(@path)) if File.readable?(@path)
14
+ end
15
+
16
+ def dump(obj, force_fsync = false)
17
+ yaml = obj.to_hsh.to_yaml.b
18
+
19
+ # do not replace existing state file if there are no changes
20
+ # this will be racy if we ever do async dumps or shared state
21
+ # files, but we don't do that...
22
+ return if File.readable?(@path) && IO.binread(@path) == yaml
23
+
24
+ dir = File.dirname(@path)
25
+ Tempfile.open(%w(player.state .tmp), dir) do |tmp|
26
+ tmp.binmode
27
+ tmp.write(yaml)
28
+ tmp.flush
29
+ tmp.fsync if @do_fsync || force_fsync
30
+ File.rename(tmp.path, @path)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,76 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require 'socket'
5
+ require 'io/wait'
6
+
7
+ class DTAS::UNIXAccepted # :nodoc:
8
+ attr_reader :to_io
9
+
10
+ def initialize(sock)
11
+ @to_io = sock
12
+ @send_buf = []
13
+ end
14
+
15
+ # public API (for DTAS::Player)
16
+ # returns :wait_readable on success
17
+ def emit(msg)
18
+ buffered = @send_buf.size
19
+ if buffered == 0
20
+ begin
21
+ @to_io.sendmsg_nonblock(msg, Socket::MSG_EOR)
22
+ return :wait_readable
23
+ rescue Errno::EAGAIN
24
+ @send_buf << msg
25
+ return :wait_writable
26
+ rescue => e
27
+ return e
28
+ end
29
+ elsif buffered > 100
30
+ return RuntimeError.new("too many messages buffered")
31
+ else # buffered > 0
32
+ @send_buf << msg
33
+ return :wait_writable
34
+ end
35
+ end
36
+
37
+ # flushes pending data if it got buffered
38
+ def writable_iter
39
+ begin
40
+ msg = @send_buf.shift or return :wait_readable
41
+ @to_io.send_nonblock(msg, Socket::MSG_EOR)
42
+ rescue Errno::EAGAIN
43
+ @send_buf.unshift(msg)
44
+ return :wait_writable
45
+ rescue => e
46
+ return e
47
+ end while true
48
+ end
49
+
50
+ def readable_iter
51
+ io = @to_io
52
+ nread = io.nread
53
+
54
+ # EOF, assume no spurious wakeups for SOCK_SEQPACKET
55
+ return nil if nread == 0
56
+
57
+ begin
58
+ begin
59
+ msg, _, _ = io.recvmsg_nonblock(nread)
60
+ rescue EOFError, SystemCallError
61
+ return nil
62
+ end
63
+ yield(self, msg) # DTAS::Player deals with this
64
+ nread = io.nread
65
+ end while nread > 0
66
+ :wait_readable
67
+ end
68
+
69
+ def close
70
+ @to_io.close
71
+ end
72
+
73
+ def closed?
74
+ @to_io.closed?
75
+ end
76
+ end
@@ -0,0 +1,51 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require 'dtas'
5
+ require 'socket'
6
+ require 'io/wait'
7
+ require 'shellwords'
8
+
9
+ class DTAS::UNIXClient # :nodoc:
10
+ attr_reader :to_io
11
+
12
+ def self.default_path
13
+ (ENV["DTAS_PLAYER_SOCK"] || File.expand_path("~/.dtas/player.sock")).b
14
+ end
15
+
16
+ def initialize(path = self.class.default_path)
17
+ @to_io = begin
18
+ raise if ENV["_DTAS_NOSEQPACKET"]
19
+ Socket.new(:AF_UNIX, :SOCK_SEQPACKET, 0)
20
+ rescue
21
+ warn("get your operating system developers to support " \
22
+ "SOCK_SEQPACKET for AF_UNIX sockets")
23
+ warn("falling back to SOCK_DGRAM, reliability possibly compromised")
24
+ Socket.new(:AF_UNIX, :SOCK_DGRAM, 0)
25
+ end
26
+ @to_io.connect(Socket.pack_sockaddr_un(path))
27
+ end
28
+
29
+ def req_start(args)
30
+ args = Shellwords.join(args) if Array === args
31
+ @to_io.send(args, Socket::MSG_EOR)
32
+ end
33
+
34
+ def req_ok(args, timeout = nil)
35
+ res = req(args, timeout)
36
+ res == "OK" or raise "Unexpected response: #{res}"
37
+ res
38
+ end
39
+
40
+ def req(args, timeout = nil)
41
+ req_start(args)
42
+ res_wait(timeout)
43
+ end
44
+
45
+ def res_wait(timeout = nil)
46
+ @to_io.wait(timeout)
47
+ nr = @to_io.nread
48
+ nr > 0 or raise EOFError, "unexpected EOF from server"
49
+ @to_io.recvmsg[0]
50
+ end
51
+ end
@@ -0,0 +1,110 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require 'socket'
5
+ require_relative '../dtas'
6
+ require_relative 'unix_accepted'
7
+
8
+ # This uses SOCK_SEQPACKET, unlike ::UNIXServer in Ruby stdlib
9
+
10
+ # The programming model for the event loop here aims to be compatible
11
+ # with EPOLLONESHOT use with epoll, since that fits my brain far better
12
+ # than existing evented APIs/frameworks.
13
+ # If we cared about scalability to thousands of clients, we'd really use epoll,
14
+ # but IO.select can be just as fast (or faster) with few descriptors and
15
+ # is obviously more portable.
16
+
17
+ class DTAS::UNIXServer # :nodoc:
18
+ attr_reader :to_io
19
+
20
+ def close
21
+ File.unlink(@path)
22
+ @to_io.close
23
+ end
24
+
25
+ def initialize(path)
26
+ @path = path
27
+ # lock down access by default, arbitrary commands may run as the
28
+ # same user dtas-player runs as:
29
+ old_umask = File.umask(0077)
30
+ @to_io = Socket.new(:AF_UNIX, :SOCK_SEQPACKET, 0)
31
+ addr = Socket.pack_sockaddr_un(path)
32
+ begin
33
+ @to_io.bind(addr)
34
+ rescue Errno::EADDRINUSE
35
+ # maybe we have an old path leftover from a killed process
36
+ tmp = Socket.new(:AF_UNIX, :SOCK_SEQPACKET, 0)
37
+ begin
38
+ tmp.connect(addr)
39
+ raise RuntimeError, "socket `#{path}' is in use", []
40
+ rescue Errno::ECONNREFUSED
41
+ # ok, leftover socket, unlink and rebind anyways
42
+ File.unlink(path)
43
+ @to_io.bind(addr)
44
+ ensure
45
+ tmp.close
46
+ end
47
+ end
48
+ @to_io.listen(1024)
49
+ @readers = { self => true }
50
+ @writers = {}
51
+ ensure
52
+ File.umask(old_umask)
53
+ end
54
+
55
+ def write_failed(client, e)
56
+ warn "failed to write to #{client}: #{e.message} (#{e.class})"
57
+ client.close
58
+ end
59
+
60
+ def readable_iter
61
+ # we do not do anything with the block passed to us
62
+ begin
63
+ sock, _ = @to_io.accept_nonblock
64
+ @readers[DTAS::UNIXAccepted.new(sock)] = true
65
+ rescue Errno::ECONNABORTED # ignore this, it happens
66
+ rescue Errno::EAGAIN
67
+ return :wait_readable
68
+ end while true
69
+ end
70
+
71
+ def wait_ctl(io, err)
72
+ case err
73
+ when :wait_readable
74
+ @readers[io] = true
75
+ when :wait_writable
76
+ @writers[io] = true
77
+ when :delete
78
+ @readers.delete(io)
79
+ @writers.delete(io)
80
+ when :ignore
81
+ # There are 2 cases for :ignore
82
+ # - DTAS::Buffer was readable before, but all destinations (e.g. sinks)
83
+ # were blocked, so we stop caring for producer (buffer) readability.
84
+ # - a consumer (e.g. DTAS::Sink) just became writable, but the
85
+ # corresponding DTAS::Buffer was already readable in a previous
86
+ # call.
87
+ when nil
88
+ io.close
89
+ when StandardError
90
+ io.close
91
+ else
92
+ raise "BUG: wait_ctl invalid: #{io} #{err.inspect}"
93
+ end
94
+ end
95
+
96
+ def run_once
97
+ begin
98
+ # give IO.select one-shot behavior, snapshot and replace the watchlist
99
+ r = IO.select(@readers.keys, @writers.keys) or return
100
+ r[1].each do |io|
101
+ @writers.delete(io)
102
+ wait_ctl(io, io.writable_iter)
103
+ end
104
+ r[0].each do |io|
105
+ @readers.delete(io)
106
+ wait_ctl(io, io.readable_iter { |_io, msg| yield(_io, msg) })
107
+ end
108
+ end
109
+ end
110
+ end
data/lib/dtas/util.rb ADDED
@@ -0,0 +1,15 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require_relative '../dtas'
5
+
6
+ # in case we need to convert DB values to a linear scale
7
+ module DTAS::Util # :nodoc:
8
+ def db_to_linear(val)
9
+ Math.exp(val * Math.log(10) * 0.05)
10
+ end
11
+
12
+ def linear_to_db(val)
13
+ Math.log10(val) * 20
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require_relative '../dtas'
5
+
6
+ module DTAS::WritableIter # :nodoc:
7
+ attr_accessor :on_writable
8
+
9
+ def writable_iter_init
10
+ @on_writable = nil
11
+ end
12
+
13
+ # this is used to exchange our own writable status for the readable
14
+ # status of the DTAS::Buffer which triggered us.
15
+ def writable_iter
16
+ if owr = @on_writable
17
+ @on_writable = nil
18
+ owr.call # this triggers readability watching of DTAS::Buffer
19
+ end
20
+ :ignore
21
+ end
22
+ end
data/perl/dtas-graph ADDED
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/perl -w
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ use strict;
5
+ use Graph::Easy; # for ASCII-art graphs
6
+ $^O =~ /linux/ or print STDERR "$0 probably only works on Linux...\n";
7
+ scalar @ARGV or die "Usage: $0 PID [PID ...]";
8
+ our $procfs = $ENV{PROCFS} || "/proc";
9
+ my $cull_self_pipe = 1;
10
+
11
+ # returns a list of PIDs which are children of the given PID
12
+ sub children_of {
13
+ my ($ppid) = @_;
14
+ my %rv = map {
15
+ s/\A\s*//g;
16
+ s/\s*\z//g;
17
+ my ($pid, $cmd) = split(/\s+/, $_, 2);
18
+ $pid => $cmd;
19
+ } `ps h -o pid,cmd --ppid=$ppid`;
20
+ \%rv;
21
+ }
22
+
23
+ # pid => [ child pids ]
24
+ my %pids;
25
+
26
+ # pipe_ino => { r => [ [pid, fd], [pid, fd] ], w => [ [pid, fd], ... ] }
27
+ my %pipes;
28
+
29
+ # pid => argv
30
+ my %cmds;
31
+
32
+ my $pipe_nr = 0;
33
+ # pipe_id -> pipe_ino (we use short pipe IDs to save space on small terms)
34
+ my %graphed;
35
+
36
+ my @to_scan = (@ARGV);
37
+
38
+ sub cmd_of {
39
+ my ($pid) = @_;
40
+ my $cmd = `ps h -o cmd $pid`;
41
+ chomp $cmd;
42
+ $cmd;
43
+ }
44
+
45
+ while (my $pid = shift @to_scan) {
46
+ my $children = children_of($pid);
47
+ my @child_pids = keys %$children;
48
+ push @to_scan, @child_pids;
49
+ $pids{$pid} = \@child_pids;
50
+ foreach my $child (keys @child_pids) {
51
+ $cmds{$child} = $children->{$child};
52
+ }
53
+ }
54
+
55
+ # build up a hash of pipes and their connectivity to processes:
56
+ #
57
+ foreach my $pid (keys %pids) {
58
+ my @out = `lsof -p $pid`;
59
+ # output is like this:
60
+ # play 12739 ew 0r FIFO 0,7 0t0 36924019 pipe
61
+ foreach my $l (@out) {
62
+ my @l = split(/\s+/, $l);
63
+ $l[4] eq "FIFO" or next;
64
+
65
+ my $fd = $l[3];
66
+ my $pipe_ino = $l[7];
67
+ my $info = $pipes{$pipe_ino} ||= { r => [], w => [] };
68
+ if ($fd =~ s/r\z//) {
69
+ push @{$info->{r}}, [ $pid, $fd ];
70
+ } elsif ($fd =~ s/w\z//) {
71
+ push @{$info->{w}}, [ $pid, $fd ];
72
+ }
73
+
74
+ }
75
+ }
76
+
77
+ my $graph = Graph::Easy->new();
78
+ foreach my $pid (keys %pids) {
79
+ $graph->add_node($pid);
80
+ }
81
+
82
+ foreach my $pipe_ino (keys %pipes) {
83
+ my $info = $pipes{$pipe_ino};
84
+ my %pairs;
85
+ my $pipe_node;
86
+
87
+ foreach my $rw (qw(r w)) {
88
+ foreach my $pidfd (@{$info->{$rw}}) {
89
+ my ($pid, $fd) = @$pidfd;
90
+ my $pair = $pairs{$pid} ||= {};
91
+ my $fds = $pair->{$rw} ||= [];
92
+ push @$fds, $fd;
93
+ }
94
+ }
95
+ # use Data::Dumper;
96
+ # print Dumper(\%pairs);
97
+ my $nr_pids = scalar keys %pairs;
98
+
99
+ foreach my $pid (keys %pairs) {
100
+ my $pair = $pairs{$pid};
101
+ my $r = $pair->{r} || [];
102
+ my $w = $pair->{w} || [];
103
+ next if $cull_self_pipe && $nr_pids == 1 && @$r && @$w;
104
+
105
+ unless ($pipe_node) {
106
+ my $pipe_id = $pipe_nr++;
107
+ $graphed{$pipe_id} = $pipe_ino;
108
+ $pipe_node = "|$pipe_id";
109
+ $graph->add_node($pipe_node);
110
+ }
111
+
112
+ $graph->add_edge($pipe_node, $pid, join(',', @$r)) if @$r;
113
+ $graph->add_edge($pid, $pipe_node, join(',', @$w)) if @$w;
114
+ }
115
+ }
116
+
117
+ print " PID COMMAND\n";
118
+ foreach my $pid (sort { $a <=> $b } keys %pids) {
119
+ printf "% 6d", $pid;
120
+ print " ", $cmds{$pid} || cmd_of($pid), "\n";
121
+ }
122
+
123
+ print "\nPIPEID PIPE_INO\n";
124
+ foreach my $pipe_id (sort { $a <=> $b } keys %graphed) {
125
+ printf "% 6s", "|$pipe_id";
126
+ print " ", $graphed{$pipe_id}, "\n";
127
+ }
128
+
129
+ print $graph->as_ascii;