Re: bug#54458: 27.2; erc-dcc-get: Re-entering top level after C stack overflow

"J.P." <[email protected]>
Newsgroups gmane.emacs.erc.general
Message-ID <[email protected]>
Hi Fernando,

In your initial report, you mentioned having trouble receiving multiple
files.

> If I transfer multiple files (three or four), sometimes with sizes
> smaller than those mentioned above, the C stack overflow hits way
> earlier.

Did you mean successively or simultaneously? Because when attempting the
latter, I can't even get off the ground. Specifically, when I try to get
two transfers going [1], the first freezes the moment the second starts.
And no further packets are exchanged for the first connection. So, as
far as simultaneous transfers are concerned, it may be that the changes
you so kindly tried didn't introduce a regression after all and that a
preexisting (though possibly related) bug has emerged.

>> The only different behaviors I noticed in Emacs was the fact that it
>> stops responding when transfers go beyond a certain percentage of
>> completion (I couldn't be sure of the number, but in my experiments it
>> happens above 45%) and the impossibility of transferring more than one
>> file at the same time.

That last part sounds like it may be similar to what I ran into. If so,
we may be in luck. It appears that, with a couple simple tweaks, I'm
able to successfully complete simultaneous transfers of large files.
However, retaining a responsive Emacs is another issue. Assuming the
sender misbehaves and the changes you last tried are also applied, I
lose control of Emacs the instant a send is blocked and only regain it
once all (simultaneous) transfers have completed, which seems more or
less in line with what you describe [2].

When you get a chance, please try the proposed multi-file fix, even
though it does nothing for the unresponsiveness problem. Also, if it's
not too much trouble, would you mind doing something like

  # tcpdump -i eno1 -Uw ./dump 'host 93.184.216.34 and tcp port 9899'

from before connecting until the unresponsiveness starts and then
uploading ./dump somewhere (like an s3 bucket)? Thanks.


[1] On Emacs 29, without any of the proposed changes applied:
    a. Start two emacs -Q instances, a sender and a receiver
    b. Start two helper scripts, each serving a different large file
    c. Offer both files on the sender
    d. Accept both files on the receiver

[2] To be clear, I'm still able to issue a quit signal, which results in
    a message about an error in the process filter. However, it does
    nothing to interrupt the actual process (info "(emacs) Quitting").
    And, FWIW, the first blocked send attempt never actually returns to
    the calling process filter, at least in my crude simulation.
0003-Allow-matching-against-string-values-in-erc-dcc-memb.patch (text/x-patch, 1.4 KB)
From f54f32465ed3d7a3206a98987943de13c39aa479 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Sat, 9 Apr 2022 23:32:22 -0700
Subject: [PATCH 3/3] Allow matching against string values in erc-dcc-member

* lisp/erc/erc-dcc.el (erc-dcc-member): Be more tolerant in the
catch-all case by testing for equality instead of identity.
(erc-dcc-do-GET-command): Pass filename when querying
`erc-dcc-member'.
---
 lisp/erc/erc-dcc.el | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lisp/erc/erc-dcc.el b/lisp/erc/erc-dcc.el
index 636e5b20b1..c6871aefd3 100644
--- a/lisp/erc/erc-dcc.el
+++ b/lisp/erc/erc-dcc.el
@@ -196,7 +196,7 @@ erc-dcc-member
                       (erc-extract-nick test)
                       (erc-extract-nick val)))
                 ;; not a nick
-                (eq test val)
+                (equal test val)
                 (setq cont nil))))
         (if cont
             (setq result elt)
@@ -507,7 +507,7 @@ erc-dcc-do-GET-command
 re-join the arguments, separated by a space.
 PROC is the server process."
   (setq file (and file (mapconcat #'identity file " ")))
-  (let* ((elt (erc-dcc-member :nick nick :type 'GET))
+  (let* ((elt (erc-dcc-member :nick nick :type 'GET :file file))
          (filename (or file (plist-get elt :file) "unknown")))
     (if elt
         (let* ((file (read-file-name
-- 
2.35.1
serve.py (application/octet-stream, 2.8 KB)
"""Hostile DCC-SEND endpoint

Usage: python this_script.py ./blob.bin [starve|ignore] [port]

By default, simulate a pedantic client that waits on checksums.  With
``starve``, don't wait for read receipts before sending the next chunk.
With ``ignore``, behave like some real-world clients and treat receipts
as heartbeats.

"""
import sys
import enum
import socket
import asyncio

from pathlib import Path


class Mode(enum.Enum):
    normal = enum.auto()
    starve = enum.auto()
    ignore = enum.auto()


class OnConnect:
    file: Path
    mode: Mode

    def __init__(self, file: str, mode: str = "normal"):
        self.file = Path(file)
        self.mode = Mode[mode]
        print(f"Sending {file!r} ({self.file.stat().st_size} bytes)")
        print("Mode:", self.mode.name)

    async def read(self) -> tuple[bytes, int]:
        data = await self.reader.read(1024)
        dlen = len(data)
        print("." if dlen == 4 else f"[{dlen}]", end="", flush=True)
        return (data, dlen)

    async def finish(self, sent: int, received: int):
        try:
            while g := await asyncio.wait_for(self.read(), timeout=1):
                received += g[1]
        except asyncio.TimeoutError:
            pass
        print("\nSent %d bytes" % sent)
        print("Saw %d reports" % (received // 4))
        self.writer.close()
        await self.writer.wait_closed()

    async def handle(self):
        sent = received = 0
        print(f"Connection from {self.writer.get_extra_info('peername')!r}")

        with self.file.open("rb") as f:
            while chunk := f.read(32768):
                self.writer.write(chunk)
                await self.writer.drain()
                sent += len(chunk)
                if self.mode is Mode.starve:
                    continue
                last = cur = 0
                while cur < sent:
                    last, n = await self.read()
                    received += n
                    if not cur and self.mode is Mode.ignore:
                        break
                    cur = int(last[-4:].hex(), 16)

        await self.finish(sent, received)

    def __call__(self, reader, writer):
        self.reader = reader
        self.writer = writer
        return self.handle()


async def main(file: str, mode: str = 'normal', port: str = '0'):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 128)
    sock.bind(("127.0.0.1", int(port)))
    server = await asyncio.start_server(OnConnect(file, mode), sock=sock)
    print(f"Serving on {server.sockets[0].getsockname()}")

    async with server:
        await server.serve_forever()


if __name__ == "__main__":
    try:
        asyncio.run(main(*sys.argv[1:]))
    except KeyboardInterrupt:
        print()
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.