about summary refs log tree commit homepage
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/Devel/Mwrap.pm21
-rw-r--r--lib/Devel/Mwrap/PSGI.pm190
-rw-r--r--lib/Devel/Mwrap/Rproxy.pm220
-rw-r--r--lib/mwrap/.gitignore1
-rw-r--r--lib/mwrap_rack.rb126
5 files changed, 127 insertions, 431 deletions
diff --git a/lib/Devel/Mwrap.pm b/lib/Devel/Mwrap.pm
deleted file mode 100644
index 2691802..0000000
--- a/lib/Devel/Mwrap.pm
+++ /dev/null
@@ -1,21 +0,0 @@
-# Copyright (C) all contributors <mwrap-perl@80x24.org>
-# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
-package Devel::Mwrap;
-use v5.12;
-our $VERSION = '0.0.0';
-use XSLoader;
-XSLoader::load(__PACKAGE__, $VERSION);
-
-# allow using via the "-d:Mwrap" switch on the command-line:
-package # hide the package from the PAUSE indexer
-        DB;
-
-sub DB {} # noop, just keeps "-d:Mwrap" happy
-
-1;
-__END__
-=pod
-
-=head1 NAME
-
-Devel::Mwrap - LD_PRELOAD malloc wrapper + line stats for Perl
diff --git a/lib/Devel/Mwrap/PSGI.pm b/lib/Devel/Mwrap/PSGI.pm
deleted file mode 100644
index 3a3a29b..0000000
--- a/lib/Devel/Mwrap/PSGI.pm
+++ /dev/null
@@ -1,190 +0,0 @@
-# Copyright (C) all contributors <mwrap@80x24.org>
-# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
-#
-# Note: this is deprecated, use httpd.h instead
-package Devel::Mwrap::PSGI;
-use v5.12; # strict
-use warnings;
-use Devel::Mwrap;
-use Fcntl qw(SEEK_SET);
-
-sub new {
-        my ($class) = @_;
-        bless {}, $class;
-}
-
-my %HTML_ESC = (
-        '&' => '&amp;',
-        '>' => '&gt;',
-        '<' => '&lt;',
-        '"' => '&quot;',
-        "'" => '&#39;'
-);
-
-sub encode_html {
-        my ($str) = @_;
-        $str =~ s/[&><"']/$HTML_ESC{$1}/sge;
-        $str;
-}
-
-my %URI_ESC;
-for (0..255) { $URI_ESC{chr($_)} = sprintf('%%%02X', $_) }
-sub uri_escape {
-        my ($str) = @_;
-        $str =~ s/([^A-Za-z0-9\-\._~])/$URI_ESC{$1}/sge;
-        $str;
-}
-
-sub uri_unescape {
-        my ($str) = @_;
-        $str =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/sge;
-        $str;
-}
-
-my @FIELDS = qw(bytes allocations frees live
-                mean_life max_life location);
-my $HDR = '<tr><th>' . join('</th><th>', @FIELDS) . '</th></tr>';
-
-sub accumulate_i { # callback for Devel::Mwrap::each
-        my ($all, $src_loc) = @_;
-        my $alloc = $src_loc->allocations;
-        my $frees = $src_loc->frees;
-        push @$all, [ $src_loc->total - $src_loc->freed_bytes,
-                        $alloc, $frees, $alloc - $frees,
-                        $src_loc->mean_lifespan, $src_loc->max_lifespan,
-                        $src_loc->name ];
-}
-
-sub fh_response {
-        my ($fh) = @_;
-        $fh->flush or die "flush: $!";
-        seek($fh, 0, SEEK_SET) or die "seek: $!";
-        [ 200, [
-                'Expires' => 'Fri, 01 Jan 1980 00:00:00 GMT',
-                'Pragma' => 'no-cache',
-                'Cache-Control' => 'no-cache, max-age=0, must-revalidate',
-                'Content-Type' => 'text/html; charset=UTF-8',
-                'Content-Length' => -s $fh
-        ], $fh];
-}
-
-sub each_gt {
-        my ($env, $min) = @_;
-        my ($sort) = ($env->{QUERY_STRING} =~ /\bsort=(\w+)\b/a);
-        open my $fh, '+>', undef or die "open: $!";
-        $sort //= 'bytes';
-        my $sn = $env->{SCRIPT_NAME};
-        my $t = "Devel::Mwrap::each($min)";
-        my $all = [];
-        my @f = @FIELDS;
-        my $sc = 0;
-        for (my $i = 0; $i <= $#FIELDS; $i++) {
-                if ($FIELDS[$i] eq $sort) {
-                        $sc = $i;
-                        last;
-                }
-        }
-        $f[$sc] = "<b>$f[$sc]</b>";
-        @f = (join('</th><th>', map {;
-                if (substr($_, 0, 1) eq '<') {
-                        $_;
-                } else {
-                        qq(<a\nhref="$sn/each/$min?sort=$_">$_</a>);
-                }
-        } @f));
-        my @all;
-        Devel::Mwrap::each($min, \&accumulate_i, \@all);
-        if ($sc eq $#FIELDS) { # locations are sorted alphabetically
-                @all = sort { $a->[$sc] cmp $b->[$sc] } @all;
-        } else { # everything else is numeric
-                @all = sort { $b->[$sc] <=> $a->[$sc] } @all;
-        }
-        my $age = Devel::Mwrap::current_age();
-        my $live = $age - Devel::Mwrap::total_bytes_freed();
-        print $fh <<EOM;
-<html><head><title>$t</title></head><body><p>$t
-<p>Current age: $age (live: $live)
-<table><tr><th>@f</th></tr>
-EOM
-        while (my $cols = shift @all) {
-                # cols: [ bytes , allocations, frees, mean_lifespan,
-                #   max_lifespan, name ]
-                my $loc_name = pop @$cols;
-                $cols->[4] = sprintf('%0.3f', $cols->[4]); # mean_life
-                my $href = "$sn/at/".uri_escape($loc_name);
-                print $fh '<tr><td>', join('</td><td>', @$cols),
-                        qq(<td><a\nhref="),
-                                encode_html($href),
-                        qq(">), encode_html($loc_name),
-                        "</a></td></tr>\n";
-        }
-        print $fh "</table></body></html>\n";
-        fh_response($fh);
-}
-
-sub each_at_i {
-        my ($fh, $size, $gen, $addr) = @_;
-        print $fh "<tr><td>$size</td><td>$gen</td><td>";
-        printf $fh "0x%lx</td></tr>\n", $addr;
-}
-
-sub each_at {
-        my ($env, $src_loc) = @_;
-        my $t = encode_html($src_loc->name);
-        open my $fh, '+>', undef or die "open: $!";
-        my $age = Devel::Mwrap::current_age();
-        my $live = $age - Devel::Mwrap::total_bytes_freed();
-        print $fh <<EOM;
-<html><head><title>$t</title></head><body><p>live allocations at $t
-<p>Current age: $age (live: $live)\n<table>
-<tr><th>size</th><th>generation</th><th>address</th></tr>
-EOM
-        $src_loc->each(0, \&each_at_i, $fh);
-        print $fh "</table></body></html>\n";
-        fh_response($fh);
-}
-
-sub r404 {
-        my $t404 = "Not Found\n";
-        [ 404, [ qw(Content-Type text/plain
-                Content-Length), length($t404) ], [ $t404 ] ];
-}
-
-my $default_min = 2000;
-my $url = 'https://80x24.org/mwrap-perl.git/about';
-chomp(my $root = <<EOM);
-<html><head><title>Mwrap demo</title></head><body><p><a
-href="each/$default_min">allocations &gt;$default_min bytes</a><p><a
-href="$url">$url</a></body></html>
-EOM
-
-sub call { # PSGI entry point
-        Devel::Mwrap::quiet(1);
-        my (undef, $env) = @_;
-        my $ret;
-        my $path_info = $env->{PATH_INFO};
-        if ($env->{REQUEST_METHOD} eq 'GET') {
-                if ($path_info =~ m!\A/each/([0-9]+)\z!) {
-                        $ret = each_gt($env, $1 + 0);
-                } elsif ($path_info =~ m!\A/at/(.*)\z!) {
-                        my $src_loc = Devel::Mwrap::get(uri_unescape($1));
-                        $ret = $src_loc ? each_at($env, $src_loc) : r404();
-                } elsif ($path_info eq '/') {
-                        $ret = [ 200, [ qw(Content-Type text/html
-                                        Content-Length), length($root) ],
-                                [ $root ] ]
-                }
-        } elsif ($env->{REQUEST_METHOD} eq 'POST') {
-                if ($path_info eq '/reset') {
-                        Devel::Mwrap::reset();
-                        $ret = [ 200, [ qw(Content-Type text/html
-                                        Content-Length 5) ],
-                                [ "done\n" ] ];
-                }
-        }
-        $ret //= r404();
-        Devel::Mwrap::quiet(0);
-        $ret;
-}
-
-1;
diff --git a/lib/Devel/Mwrap/Rproxy.pm b/lib/Devel/Mwrap/Rproxy.pm
deleted file mode 100644
index d5a9d9d..0000000
--- a/lib/Devel/Mwrap/Rproxy.pm
+++ /dev/null
@@ -1,220 +0,0 @@
-# Copyright (C) mwrap hackers <mwrap-perl@80x24.org>
-# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
-
-# minimal reverse proxy to expose the embedded httpd.h UNIX sockets
-# via PSGI (and thus TCP HTTP/1.x).  This does not have a hard dependency
-# on Mwrap.so.
-#
-# Warning: this has a synchronous wait dependency, so isn't suited for
-# non-blocking async HTTP servers.
-package Devel::Mwrap::Rproxy;
-use v5.12; # strict
-use Fcntl qw(SEEK_SET);
-use IO::Socket::UNIX;
-use Plack::Util;
-
-sub new { bless { socket_dir => $_[1]}, $_[0] }
-
-sub r {
-        [ $_[0], [
-                'Expires' => 'Fri, 01 Jan 1980 00:00:00 GMT',
-                'Pragma' => 'no-cache',
-                'Cache-Control' => 'no-cache, max-age=0, must-revalidate',
-                'Content-Type' => 'text/html; charset=UTF-8',
-                'Content-Length' => length($_[1]),
-        ], [ $_[1] ] ];
-}
-
-my $valid_pid = $^O eq 'linux' ? sub {
-        my ($pid) = @_;
-        if (open(my $fh, '<', "/proc/$pid/cmdline")) {
-                local $/;
-                my $str = <$fh> // return;
-                $str =~ tr/\0/ /;
-                Plack::Util::encode_html($str);
-        }
-} : sub { kill(0, $_[0]) ? "PID: $_[0]" : undef };
-
-sub list {
-        my ($self, $env) = @_;
-        state $t = 'mwrap reverse proxy endpoints';
-        open(my $fh, '+>', \(my $str)) or die "open: $!";
-        print $fh '<html><head><title>', $t, '</title></head><body><pre>', $t,
-                "\n\n";
-        my $dir = $self->{socket_dir};
-        opendir(my $dh, $dir) or return r(500, "socket_dir: $!");
-        my @socks = grep(/\A[0-9]+\.sock\z/, readdir($dh));
-        my %o = (Type => SOCK_STREAM, Peer => undef);
-        for (@socks) {
-                $o{Peer} = "$dir/$_";
-                substr($_, -5, 5, ''); # chop off .sock
-                my $cmd = $valid_pid->($_) // next;
-                my $c = IO::Socket::UNIX->new(%o) // next;
-                print $fh qq(<a\nhref="./$_/">$_</a>/);
-                $_ .= '/each/2000';
-                say $fh qq(<a\nhref="./), $_, qq(">each/2000</a>\t), $cmd;
-        }
-        print $fh '</pre></body></html>';
-        r(200, $str);
-}
-
-our %addr2line; # { exe|lib => Devel::Mwrap::Rproxy::A2L }
-my %cache; # "$exe\0$addr$st_ctime$st_size" => $line
-my $cache_exp = 0;
-my $cache_time = 1800;
-
-sub resolve_exe ($$) {
-        my ($exe, $st) = @_;
-        # n.b. this assumes PATH is identical across the rproxy and
-        # mwrap-httpd process, which may not always be the case:
-        if (index($exe, '/') < 0 && !-r $exe) {
-                for my $p (split(/:/, $ENV{PATH})) {
-                        $p .= "/$exe";
-                        if (-x $p) {
-                                $exe = $p;
-                                last;
-                        }
-                }
-        }
-        my $fh;
-        if (-T $exe && open($fh, '<', $exe)) { # is it text? use shebang
-                my $l = readline($fh);
-                $exe = ($l =~ /\A\#\![ \t]*(\S+)/) ? $1 : $^X;
-        }
-        return unless -e $exe;
-        my @st = stat(_);
-
-        # Debian `perl-debug' is special:
-        if ($exe eq '/usr/bin/perl' && -x '/usr/bin/debugperl') {
-                @st = stat(_);
-                $exe = '/usr/bin/debugperl';
-        }
-        $$st = pack('dd', $st[10], $st[7]); # ctime + size
-        $exe;
-}
-
-# addr2line bidirectional pipe wrapper
-sub a2l {
-        my ($exe, $addr) = @_;
-        $exe = resolve_exe($exe, \(my $st)) // return "$exe($addr)";
-        $cache{"$addr\0$exe$st"} //= do {
-                my $a2l = $addr2line{$exe} //=
-                        Devel::Mwrap::Rproxy::A2L->new($exe);
-
-                $a2l ? do {
-                        chomp(my $line = $a2l->lookup($addr));
-                        $line =~ s/\Q?? at ??:0\E//; # FreeBSD
-                        $line = Plack::Util::encode_html($line);
-                        $line =~ /\?\?/ ? "$line $exe($addr)" :
-                                ($line =~ /\S/ ? $line : "$exe($addr)");
-                } : "$exe($addr)"
-        }
-}
-
-sub call { # PSGI entry point
-        my ($self, $env) = @_;
-        my $uri = $env->{REQUEST_URI};
-        $uri =~ s!\A\Q$env->{SCRIPT_NAME}\E!!;
-        my $method = $env->{REQUEST_METHOD};
-        return list(@_) if $uri eq '/' && $method eq 'GET';
-
-        # must have /$PID/ prefix to map socket
-        $uri =~ m!\A/([0-9]+)/! or return r(404, 'not found');
-        my $s = "$self->{socket_dir}/$1.sock";
-        my %o = (Peer => $s, Type => SOCK_STREAM);
-        my $c = IO::Socket::UNIX->new(%o) or return r(500, "connect: $!");
-        my $h = "$method $uri HTTP/1.0\n\n";
-        $s = send($c, $h, MSG_NOSIGNAL) // return r(500, "send: $!");
-        $s == length($h) or return r(500, "send $s <".length($h));
-        # this only expects httpd.h output, so no continuation lines:
-        $h = do { local $/ = "\r\n\r\n"; <$c> } // return r(500, "read: $!");
-        my ($code, @hdr) = split(/\r\n/, $h);
-        @hdr = grep(!/^Content-Length:/i, @hdr); # addr2line changes length
-        my $csv = grep(m!^Content-Type: text/csv!i, @hdr);
-        (undef, $code, undef) = split(/ /, $code);
-        @hdr = map { split(/: /, $_, 2) } @hdr;
-        sub {
-                my ($wcb) = @_;
-                my $http_out = $wcb->([$code, \@hdr]);
-                my $now = time;
-                if ($now > $cache_exp) {
-                        undef %cache;
-                        $cache_exp = $now + $cache_time;
-                }
-
-                # GNU addr2line is slow with high bt:, and FreeBSD addr2line
-                # seems less capable.  And we can't see addr2line in this
-                # anyways since we kill them at the end of this scope.
-                # So just disable MWRAP, here:
-                delete local $ENV{MWRAP};
-                delete local $ENV{LD_PRELOAD};
-                eval {
-                        local %addr2line;
-                        # extract executable|library(address)
-                        if ($csv) {
-                                while (<$c>) {
-                                        s/\\n/\0\0/g;
-                                        s!(["\0])
-                                                ([^\("\0]+) # exe
-                                                \(([^\)"\0]+)\) # addr
-                                                (["\0])!
-                                                $1.a2l($2,$3).$4!gex;
-                                        s/\0\0/\\n/g;
-                                        $http_out->write($_);
-                                }
-                        } else {
-                                while (<$c>) {
-                                        s!>
-                                                ([^\(<]+) # exe
-                                                \(([^\)<]+)\) # addr
-                                                <!
-                                                '>'.a2l($1,$2).'<'!gex;
-                                        $http_out->write($_);
-                                }
-                        }
-                        close $c;
-                };
-                warn "E: $@" if $@;
-                $http_out->close;
-        }
-}
-
-# requires GNU addr2line for stdin/stdout support
-package Devel::Mwrap::Rproxy::A2L;
-use v5.12;
-
-sub new {
-        my ($cls, $exe) = @_;
-        pipe(my ($rd, $_wr)) or die "pipe: $!";
-        pipe(my ($_rd, $wr)) or die "pipe: $!";
-        # -f/--functions needs -p/--pretty-print to go with it
-        my $addr2line = $ENV{ADDR2LINE} // 'addr2line -i -p -f';
-        my @addr2line = split(/\s+/, $addr2line);
-        my $pid = fork // die "fork: $!";
-        if ($pid == 0) {
-                close $rd;
-                close $wr;
-                open STDIN, '<&', $_rd or die "STDIN: $!";
-                open STDOUT, '>&', $_wr or die "STDOUT: $!";
-                exec @addr2line, '-e', $exe;
-                die "exec @addr2line -e $exe: $!";
-        }
-        $_rd = $_wr = undef;
-        $wr->autoflush(1);
-        bless { rd => $rd, wr => $wr, pid => $pid }, __PACKAGE__;
-}
-
-sub lookup {
-        my ($self, $addr) = @_;
-        $addr =~ s/\A\+//;
-        say { $self->{wr} } $addr;
-        readline($self->{rd});
-}
-
-sub DESTROY {
-        my ($self) = @_;
-        close($_) for (delete @$self{qw(wr rd)});
-        waitpid(delete $self->{pid}, 0);
-}
-
-1;
diff --git a/lib/mwrap/.gitignore b/lib/mwrap/.gitignore
new file mode 100644
index 0000000..07c0394
--- /dev/null
+++ b/lib/mwrap/.gitignore
@@ -0,0 +1 @@
+version.rb
diff --git a/lib/mwrap_rack.rb b/lib/mwrap_rack.rb
new file mode 100644
index 0000000..6cc6d31
--- /dev/null
+++ b/lib/mwrap_rack.rb
@@ -0,0 +1,126 @@
+# Copyright (C) all contributors <mwrap-public@80x24.org>
+# License: GPL-2.0+ <https://www.gnu.org/licenses/gpl-2.0.txt>
+# frozen_string_literal: true
+require 'mwrap'
+require 'rack'
+require 'cgi'
+
+# MwrapRack is an obsolete standalone Rack application which can be
+# mounted to run within your application process.
+#
+# The embedded mwrap-httpd for Unix sockets and mwrap-rproxy for TCP
+# from the Perl version <https://80x24.org/mwrap-perl.git/> replaces
+# this in a non-obtrusive way for code which can't handle Ruby-level
+# threads.
+#
+# The remaining documentation remains for historical purposes:
+#
+# Using the Rack::Builder API in config.ru, you can map it to
+# the "/MWRAP/" endpoint.  As with the rest of the Mwrap API,
+# your Rack server needs to be spawned with the mwrap(1)
+# wrapper to enable the LD_PRELOAD.
+#
+#     require 'mwrap_rack'
+#     map('/MWRAP') { run(MwrapRack.new) }
+#     map('/') { run(your_normal_app) }
+#
+# This module is only available in mwrap 2.0.0+
+class MwrapRack
+  module HtmlResponse # :nodoc:
+    def response
+      [ 200, {
+          'expires' => 'Fri, 01 Jan 1980 00:00:00 GMT',
+          'pragma' => 'no-cache',
+          'cache-control' => 'no-cache, max-age=0, must-revalidate',
+          'content-type' => 'text/html; charset=UTF-8',
+        }, self ]
+    end
+  end
+
+  class Each < Struct.new(:script_name, :min, :sort) # :nodoc:
+    include HtmlResponse
+    HEADER = '<tr><th>' + %w(total allocations frees mean_life max_life
+                location).join('</th><th>') + '</th></tr>'
+    FIELDS = %w(total allocations frees mean_life max_life location)
+    def each
+      Mwrap.quiet do
+        t = -"Mwrap.each(#{min})"
+        sn = script_name
+        all = []
+        f = FIELDS.dup
+        sc = FIELDS.index(sort || 'total') || 0
+        f[sc] = -"<b>#{f[sc]}</b>"
+        f.map! do |hdr|
+          if hdr.start_with?('<b>')
+            hdr
+          else
+            -%Q(<a\nhref="#{sn}/each/#{min}?sort=#{hdr}">#{hdr}</a>)
+          end
+        end
+        Mwrap.each(min) do |loc, total, allocations, frees, age_sum, max_life|
+          mean_life = frees == 0 ? Float::INFINITY : age_sum/frees.to_f
+          all << [total,allocations,frees,mean_life,max_life,loc]
+        end
+        all.sort_by! { |cols| -cols[sc] }
+
+        yield(-"<html><head><title>#{t}</title></head>" \
+               "<body><h1>#{t}</h1>\n" \
+               "<h2>Current generation: #{GC.count}</h2>\n<table>\n" \
+               "<tr><th>#{f.join('</th><th>')}</th></tr>\n")
+        all.each do |cols|
+          loc = cols.pop
+          cols[3] = sprintf('%0.3f', cols[3]) # mean_life
+          href = -(+"#{sn}/at/#{CGI.escape(loc)}").encode!(xml: :attr)
+          yield(%Q(<tr><td>#{cols.join('</td><td>')}<td><a\nhref=#{
+                  href}>#{-loc.encode(xml: :text)}</a></td></tr>\n))
+          cols.clear
+        end.clear
+        yield "</table></body></html>\n"
+      end
+    end
+  end
+
+  class EachAt < Struct.new(:loc) # :nodoc:
+    include HtmlResponse
+    HEADER = '<tr><th>size</th><th>generation</th></tr>'
+
+    def each
+      t = loc.name.encode(xml: :text)
+      yield(-"<html><head><title>#{t}</title></head>" \
+             "<body><h1>live allocations at #{t}</h1>" \
+             "<h2>Current generation: #{GC.count}</h2>\n<table>#{HEADER}")
+      loc.each do |size, generation|
+        yield("<tr><td>#{size}</td><td>#{generation}</td></tr>\n")
+      end
+      yield "</table></body></html>\n"
+    end
+  end
+
+  def r404 # :nodoc:
+    [404,{'content-type'=>'text/plain'},["Not found\n"]]
+  end
+
+  # The standard Rack application endpoint for MwrapRack
+  def call(env)
+    case env['PATH_INFO']
+    when %r{\A/each/(\d+)\z}
+      min = $1.to_i
+      m = env['QUERY_STRING'].match(/\bsort=(\w+)/)
+      Each.new(env['SCRIPT_NAME'], min, m ? m[1] : nil).response
+    when %r{\A/at/(.*)\z}
+      loc = -CGI.unescape($1)
+      loc = Mwrap[loc] or return r404
+      EachAt.new(loc).response
+    when '/'
+      n = 2000
+      u = 'https://80x24.org/mwrap/README.html'
+      b = -('<html><head><title>Mwrap demo</title></head>' \
+          "<body><p><a href=\"each/#{n}\">allocations &gt;#{n} bytes</a>" \
+          "<p><a href=\"#{u}\">#{u}</a>" \
+          "</body></html>\n")
+      [ 200, {'content-type'=>'text/html','content-length'=>-b.size.to_s},[b]]
+    else
+      r404
+    end
+  end
+end