Hi all.

I was experimenting with py-ansible-libssh, but I didn't went to far as
currently the port is affected by multiple issues:

- Fix pushing large files over SCP
  https://github.com/ansible/pylibssh/pull/661
- Prevent SEGFAULTs on consecutive exec_command() invocations
  https://github.com/ansible/pylibssh/pull/658
- Fix large SFTP reads
  https://github.com/ansible/pylibssh/pull/638
- sftp.get downloads only the last chunk of file
  https://github.com/ansible/pylibssh/issues/341
- SIGSEGV when running ssh_channel.exec_command() over different channels
  https://github.com/ansible/pylibssh/issues/657

Finally all related pull request are merged to upstream's develop
branch. I am backporting here commits which I was affected by and
I cannot reproduce any issues I've seen before. Well, except
significantly slower SFTP trasnfers vs SCP, but at least they
work now.

I would like to see this in, as currently for me the port as is,
in the repo is not really usable.


Index: Makefile
===================================================================
RCS file: /cvs/ports/sysutils/py-ansible-libssh/Makefile,v
diff -u -p -u -r1.3 Makefile
--- Makefile    26 Dec 2024 16:15:11 -0000      1.3
+++ Makefile    27 Mar 2025 20:52:34 -0000
@@ -3,7 +3,7 @@ COMMENT =               Python bindings for libssh sp
 DISTNAME =             ansible-pylibssh-${MODPY_DISTV}
 PKGNAME =              py-ansible-libssh-${MODPY_DISTV}
 CATEGORIES =           sysutils
-REVISION =             0
+REVISION =             1
 
 MAINTAINER =           Denis Fondras <de...@openbsd.org>
 
@@ -16,6 +16,8 @@ MODULES =             lang/python
 MODPY_PI =             Yes
 # uses own in-tree backend which wraps setuptools
 MODPY_PYBUILD =                setuptools_scm
+
+DEBUG_PACKAGES =       ${BUILD_PACKAGES}
 
 CFLAGS +=              -I${LOCALBASE}/include
 BUILD_DEPENDS =                sysutils/py-expandvars \
Index: patches/patch-src_pylibsshext_channel_pxd
===================================================================
RCS file: patches/patch-src_pylibsshext_channel_pxd
diff -N patches/patch-src_pylibsshext_channel_pxd
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ patches/patch-src_pylibsshext_channel_pxd   27 Mar 2025 20:52:34 -0000
@@ -0,0 +1,13 @@
+git -P diff 041d447a94e9dc4aead5aaed1296bdb7dffc5017^ HEAD -- src/
+
+Index: src/pylibsshext/channel.pxd
+--- src/pylibsshext/channel.pxd.orig
++++ src/pylibsshext/channel.pxd
+@@ -23,3 +23,7 @@ cdef class Channel:
+     cdef  _session
+     cdef libssh.ssh_channel _libssh_channel
+     cdef libssh.ssh_session _libssh_session
++
++cdef class ChannelCallback:
++    cdef callbacks.ssh_channel_callbacks_struct callback
++    cdef _userdata
Index: patches/patch-src_pylibsshext_channel_pyx
===================================================================
RCS file: patches/patch-src_pylibsshext_channel_pyx
diff -N patches/patch-src_pylibsshext_channel_pyx
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ patches/patch-src_pylibsshext_channel_pyx   27 Mar 2025 20:52:34 -0000
@@ -0,0 +1,53 @@
+git -P diff 041d447a94e9dc4aead5aaed1296bdb7dffc5017^ HEAD -- src/
+
+Index: src/pylibsshext/channel.pyx
+--- src/pylibsshext/channel.pyx.orig
++++ src/pylibsshext/channel.pyx
+@@ -45,6 +45,16 @@ cdef int _process_outputs(libssh.ssh_session session,
+         result.stdout += data_b
+     return len
+ 
++cdef class ChannelCallback:
++    def __cinit__(self):
++        memset(&self.callback, 0, sizeof(self.callback))
++        callbacks.ssh_callbacks_init(&self.callback)
++        self.callback.channel_data_function = 
<callbacks.ssh_channel_data_callback>&_process_outputs
++
++    def set_user_data(self, userdata):
++        self._userdata = userdata
++        self.callback.userdata = <void *>self._userdata
++
+ cdef class Channel:
+     def __cinit__(self, session):
+         self._session = session
+@@ -159,19 +169,23 @@ cdef class Channel:
+             libssh.ssh_channel_free(channel)
+             raise LibsshChannelException("Failed to open_session: 
[{0}]".format(rc))
+ 
++        result = CompletedProcess(args=command, returncode=-1, stdout=b'', 
stderr=b'')
++
++        cb = ChannelCallback()
++        cb.set_user_data(result)
++        callbacks.ssh_set_channel_callbacks(channel, &cb.callback)
++        # keep the callback around in the session object to avoid use after 
free
++        self._session.push_callback(cb)
++
+         rc = libssh.ssh_channel_request_exec(channel, command.encode("utf-8"))
+         if rc != libssh.SSH_OK:
+             libssh.ssh_channel_close(channel)
+             libssh.ssh_channel_free(channel)
+             raise LibsshChannelException("Failed to execute command [{0}]: 
[{1}]".format(command, rc))
+-        result = CompletedProcess(args=command, returncode=-1, stdout=b'', 
stderr=b'')
+ 
+-        cdef callbacks.ssh_channel_callbacks_struct cb
+-        memset(&cb, 0, sizeof(cb))
+-        cb.channel_data_function = 
<callbacks.ssh_channel_data_callback>&_process_outputs
+-        cb.userdata = <void *>result
+-        callbacks.ssh_callbacks_init(&cb)
+-        callbacks.ssh_set_channel_callbacks(channel, &cb)
++        # wait before remote writes all data before closing the channel
++        while not libssh.ssh_channel_is_eof(channel):
++            libssh.ssh_channel_poll(channel, 0)
+ 
+         libssh.ssh_channel_send_eof(channel)
+         result.returncode = libssh.ssh_channel_get_exit_status(channel)
Index: patches/patch-src_pylibsshext_includes_libssh_pxd
===================================================================
RCS file: patches/patch-src_pylibsshext_includes_libssh_pxd
diff -N patches/patch-src_pylibsshext_includes_libssh_pxd
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ patches/patch-src_pylibsshext_includes_libssh_pxd   27 Mar 2025 20:52:34 
-0000
@@ -0,0 +1,17 @@
+$ git -P log --oneline e9935cbcf62f7d2f83b9234f6db4e41a3ca480cb^..HEAD -- 
src/pylibsshext/includes/libssh.pxd
+265cae1 sftp: Increase SFTP chunk size to specification-recommended 32kB
+
+Index: src/pylibsshext/includes/libssh.pxd
+--- src/pylibsshext/includes/libssh.pxd.orig
++++ src/pylibsshext/includes/libssh.pxd
+@@ -27,6 +27,10 @@ cdef extern from "libssh/libssh.h" nogil:
+         pass
+     ctypedef ssh_session_struct* ssh_session
+ 
++    cdef struct ssh_string_struct:
++        pass
++    ctypedef ssh_string_struct* ssh_string
++
+     cdef struct ssh_key_struct:
+         pass
+     ctypedef ssh_key_struct* ssh_key
Index: patches/patch-src_pylibsshext_includes_sftp_pxd
===================================================================
RCS file: patches/patch-src_pylibsshext_includes_sftp_pxd
diff -N patches/patch-src_pylibsshext_includes_sftp_pxd
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ patches/patch-src_pylibsshext_includes_sftp_pxd     27 Mar 2025 20:52:34 
-0000
@@ -0,0 +1,61 @@
+$ git -P log --oneline e9935cbcf62f7d2f83b9234f6db4e41a3ca480cb^..HEAD -- 
src/pylibsshext/includes/sftp.pxd
+265cae1 sftp: Increase SFTP chunk size to specification-recommended 32kB
+
+Index: src/pylibsshext/includes/sftp.pxd
+--- src/pylibsshext/includes/sftp.pxd.orig
++++ src/pylibsshext/includes/sftp.pxd
+@@ -17,9 +17,11 @@
+ #
+ from posix.types cimport mode_t
+ 
+-from pylibsshext.includes.libssh cimport ssh_channel, ssh_session
++from libc cimport stdint
+ 
++from pylibsshext.includes.libssh cimport ssh_channel, ssh_session, ssh_string
+ 
++
+ cdef extern from "libssh/sftp.h" nogil:
+ 
+     struct sftp_session_struct:
+@@ -30,6 +32,31 @@ cdef extern from "libssh/sftp.h" nogil:
+         pass
+     ctypedef sftp_file_struct * sftp_file
+ 
++    struct sftp_attributes_struct:
++        char *name
++        char *longname
++        stdint.uint32_t flags
++        stdint.uint8_t type
++        stdint.uint64_t size
++        stdint.uint32_t uid
++        stdint.uint32_t gid
++        char *owner
++        char *group
++        stdint.uint32_t permissions
++        stdint.uint64_t atime64
++        stdint.uint32_t atime
++        stdint.uint32_t atime_nseconds
++        stdint.uint64_t createtime
++        stdint.uint32_t createtime_nseconds
++        stdint.uint64_t mtime64
++        stdint.uint32_t mtime
++        stdint.uint32_t mtime_nseconds
++        ssh_string acl
++        stdint.uint32_t extended_count
++        ssh_string extended_type
++        ssh_string extended_data
++    ctypedef sftp_attributes_struct * sftp_attributes
++
+     cdef int SSH_FX_OK
+     cdef int SSH_FX_EOF
+     cdef int SSH_FX_NO_SUCH_FILE
+@@ -54,6 +81,9 @@ cdef extern from "libssh/sftp.h" nogil:
+     ssize_t sftp_write(sftp_file file, const void *buf, size_t count)
+     ssize_t sftp_read(sftp_file file, const void *buf, size_t count)
+     int sftp_get_error(sftp_session sftp)
++
++    sftp_attributes sftp_stat(sftp_session session, const char *path)
++
+ 
+ cdef extern from "sys/stat.h" nogil:
+     cdef int S_IRWXU
Index: patches/patch-src_pylibsshext_scp_pyx
===================================================================
RCS file: patches/patch-src_pylibsshext_scp_pyx
diff -N patches/patch-src_pylibsshext_scp_pyx
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ patches/patch-src_pylibsshext_scp_pyx       27 Mar 2025 20:52:34 -0000
@@ -0,0 +1,48 @@
+$ git -P log --oneline a5292bdce6948e5eda91d6ff7008c7cc0f912054^..HEAD -- 
src/pylibsshext/scp.pyx
+63ab9a4 scp: Make the chunk size constant more readable
+a5292bd scp: Fix pushing large files
+
+https://github.com/ansible/pylibssh/pull/661
+
+Index: src/pylibsshext/scp.pyx
+--- src/pylibsshext/scp.pyx.orig
++++ src/pylibsshext/scp.pyx
+@@ -24,7 +24,7 @@ from pylibsshext.errors cimport LibsshSCPException
+ from pylibsshext.session cimport get_libssh_session
+ 
+ 
+-SCP_MAX_CHUNK = 65536
++SCP_MAX_CHUNK = 65_536  # 64kB
+ 
+ 
+ cdef class SCP:
+@@ -74,15 +74,25 @@ cdef class SCP:
+                 )
+ 
+             try:
++                # Read buffer
++                read_buffer_size = min(file_size, SCP_MAX_CHUNK)
++
+                 # Begin to send to the file
+                 rc = libssh.ssh_scp_push_file(scp, filename_b, file_size, 
file_mode)
+                 if rc != libssh.SSH_OK:
+                     raise LibsshSCPException("Can't open remote file: %s" % 
self._get_ssh_error_str())
+ 
+-                # Write to the open file
+-                rc = libssh.ssh_scp_write(scp, PyBytes_AS_STRING(f.read()), 
file_size)
+-                if rc != libssh.SSH_OK:
+-                    raise LibsshSCPException("Can't write to remote file: %s" 
% self._get_ssh_error_str())
++                remaining_bytes_to_read = file_size
++                while remaining_bytes_to_read > 0:
++                    # Read the chunk from local file
++                    read_bytes = min(remaining_bytes_to_read, 
read_buffer_size)
++                    read_buffer = f.read(read_bytes)
++
++                    # Write to the open file
++                    rc = libssh.ssh_scp_write(scp, 
PyBytes_AS_STRING(read_buffer), read_bytes)
++                    if rc != libssh.SSH_OK:
++                        raise LibsshSCPException("Can't write to remote file: 
%s" % self._get_ssh_error_str())
++                    remaining_bytes_to_read -= read_bytes
+             finally:
+                 libssh.ssh_scp_close(scp)
+                 libssh.ssh_scp_free(scp)
Index: patches/patch-src_pylibsshext_session_pxd
===================================================================
RCS file: patches/patch-src_pylibsshext_session_pxd
diff -N patches/patch-src_pylibsshext_session_pxd
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ patches/patch-src_pylibsshext_session_pxd   27 Mar 2025 20:52:34 -0000
@@ -0,0 +1,12 @@
+git -P diff 041d447a94e9dc4aead5aaed1296bdb7dffc5017^ HEAD -- src/
+
+Index: src/pylibsshext/session.pxd
+--- src/pylibsshext/session.pxd.orig
++++ src/pylibsshext/session.pxd
+@@ -26,5 +26,6 @@ cdef class Session:
+     cdef _hash_py
+     cdef _fingerprint_py
+     cdef _keytype_py
++    cdef _channel_callbacks
+ 
+ cdef libssh.ssh_session get_libssh_session(Session session)
Index: patches/patch-src_pylibsshext_session_pyx
===================================================================
RCS file: patches/patch-src_pylibsshext_session_pyx
diff -N patches/patch-src_pylibsshext_session_pyx
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ patches/patch-src_pylibsshext_session_pyx   27 Mar 2025 20:52:34 -0000
@@ -0,0 +1,26 @@
+git -P diff 041d447a94e9dc4aead5aaed1296bdb7dffc5017^ HEAD -- src/
+
+Index: src/pylibsshext/session.pyx
+--- src/pylibsshext/session.pyx.orig
++++ src/pylibsshext/session.pyx
+@@ -107,6 +107,10 @@ cdef class Session(object):
+         self._hash_py = None
+         self._fingerprint_py = None
+         self._keytype_py = None
++        # Due to delayed freeing of channels, some older libssh versions 
might expect
++        # the callbacks to be around even after we free the underlying 
channels so
++        # we should free them only when we terminate the session.
++        self._channel_callbacks = []
+ 
+     def __cinit__(self, host=None, **kwargs):
+         self._libssh_session = libssh.ssh_new()
+@@ -122,6 +126,9 @@ cdef class Session(object):
+                 libssh.ssh_disconnect(self._libssh_session)
+             libssh.ssh_free(self._libssh_session)
+             self._libssh_session = NULL
++
++    def push_callback(self, callback):
++        self._channel_callbacks.append(callback)
+ 
+     @property
+     def port(self):
Index: patches/patch-src_pylibsshext_sftp_pyx
===================================================================
RCS file: patches/patch-src_pylibsshext_sftp_pyx
diff -N patches/patch-src_pylibsshext_sftp_pyx
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ patches/patch-src_pylibsshext_sftp_pyx      27 Mar 2025 20:52:34 -0000
@@ -0,0 +1,106 @@
+$ git -P log --oneline e9935cbcf62f7d2f83b9234f6db4e41a3ca480cb^..HEAD -- 
src/pylibsshext/sftp.pyx
+265cae1 sftp: Increase SFTP chunk size to specification-recommended 32kB
+d3678e7 sftp: Fix overwriting existing files while downloading
+e9935cb sftp: Fix downloading files
+
+Index: src/pylibsshext/sftp.pyx
+--- src/pylibsshext/sftp.pyx.orig
++++ src/pylibsshext/sftp.pyx
+@@ -18,11 +18,15 @@
+ from posix.fcntl cimport O_CREAT, O_RDONLY, O_TRUNC, O_WRONLY
+ 
+ from cpython.bytes cimport PyBytes_AS_STRING
++from cpython.mem cimport PyMem_Free, PyMem_Malloc
+ 
+ from pylibsshext.errors cimport LibsshSFTPException
+ from pylibsshext.session cimport get_libssh_session
+ 
+ 
++SFTP_MAX_CHUNK = 32_768  # 32kB
++
++
+ MSG_MAP = {
+     sftp.SSH_FX_OK: "No error",
+     sftp.SSH_FX_EOF: "End-of-file encountered",
+@@ -63,7 +67,7 @@ cdef class SFTP:
+             rf = sftp.sftp_open(self._libssh_sftp_session, remote_file_b, 
O_WRONLY | O_CREAT | O_TRUNC, sftp.S_IRWXU)
+             if rf is NULL:
+                 raise LibsshSFTPException("Opening remote file [%s] for write 
failed with error [%s]" % (remote_file, self._get_sftp_error_str()))
+-            buffer = f.read(1024)
++            buffer = f.read(SFTP_MAX_CHUNK)
+ 
+             while buffer != b"":
+                 length = len(buffer)
+@@ -76,38 +80,54 @@ cdef class SFTP:
+                             self._get_sftp_error_str(),
+                         )
+                     )
+-                buffer = f.read(1024)
++                buffer = f.read(SFTP_MAX_CHUNK)
+             sftp.sftp_close(rf)
+ 
+     def get(self, remote_file, local_file):
+         cdef sftp.sftp_file rf
+-        cdef char read_buffer[1024]
++        cdef char *read_buffer = NULL
++        cdef sftp.sftp_attributes attrs
+ 
+         remote_file_b = remote_file
+         if isinstance(remote_file_b, unicode):
+             remote_file_b = remote_file.encode("utf-8")
+ 
++        attrs = sftp.sftp_stat(self._libssh_sftp_session, remote_file_b)
++        if attrs is NULL:
++            raise LibsshSFTPException("Failed to stat the remote file [%s]. 
Error: [%s]"
++                                      % (remote_file, 
self._get_sftp_error_str()))
++        file_size = attrs.size
++
+         rf = sftp.sftp_open(self._libssh_sftp_session, remote_file_b, 
O_RDONLY, sftp.S_IRWXU)
+         if rf is NULL:
+             raise LibsshSFTPException("Opening remote file [%s] for read 
failed with error [%s]" % (remote_file, self._get_sftp_error_str()))
+ 
+-        while True:
+-            file_data = sftp.sftp_read(rf, <void *>read_buffer, sizeof(char) 
* 1024)
+-            if file_data == 0:
+-                break
+-            elif file_data < 0:
+-                sftp.sftp_close(rf)
+-                raise LibsshSFTPException("Reading data from remote file [%s] 
failed with error [%s]"
+-                                          % (remote_file, 
self._get_sftp_error_str()))
++        try:
++            with open(local_file, 'wb') as f:
++                buffer_size = min(SFTP_MAX_CHUNK, file_size)
++                read_buffer = <char *>PyMem_Malloc(buffer_size)
++                if read_buffer is NULL:
++                    raise LibsshSFTPException("Memory allocation error")
+ 
+-            with open(local_file, 'wb+') as f:
+-                bytes_written = f.write(read_buffer[:file_data])
+-                if bytes_written and file_data != bytes_written:
+-                    sftp.sftp_close(rf)
+-                    raise LibsshSFTPException("Number of bytes [%s] read from 
remote file [%s]"
+-                                              " does not match number of 
bytes [%s] written to local file [%s]"
+-                                              " due to error [%s]"
+-                                              % (file_data, remote_file, 
bytes_written, local_file, self._get_sftp_error_str()))
++                while True:
++                    file_data = sftp.sftp_read(rf, <void *>read_buffer, 
sizeof(char) * buffer_size)
++                    if file_data == 0:
++                        break
++                    elif file_data < 0:
++                        sftp.sftp_close(rf)
++                        raise LibsshSFTPException("Reading data from remote 
file [%s] failed with error [%s]"
++                                                  % (remote_file, 
self._get_sftp_error_str()))
++
++                    bytes_written = f.write(read_buffer[:file_data])
++                    if bytes_written and file_data != bytes_written:
++                        sftp.sftp_close(rf)
++                        raise LibsshSFTPException("Number of bytes [%s] read 
from remote file [%s]"
++                                                  " does not match number of 
bytes [%s] written to local file [%s]"
++                                                  " due to error [%s]"
++                                                  % (file_data, remote_file, 
bytes_written, local_file, self._get_sftp_error_str()))
++        finally:
++            if read_buffer is not NULL:
++                PyMem_Free(read_buffer)
+         sftp.sftp_close(rf)
+ 
+     def close(self):


-- 
Regards,
 Mikolaj

Reply via email to