B4J Library An SSH library courtesy of MS Copilot - B4JSSH - now the Ferrari version

PREAMBLE

For some time now I have had a B4J app that manages a number of Netonix (any) and Ubiquiti (USW-FLEX) WISP switches that turn on/off a fleet of high end Hikvision cameras in a remote location.

This app has used the SSHJ library which has worked flawlessly.

I have decided to replace the Netonix switches with Planet (WGS-5225-8UP2SV) switches.

Unfortunately I discovered that SSHJ can't communicate with Planet switches, Copilot's explanation follows:

===========================================
Planet Switch SSH Compatibility — Technical Limitation Summary
Overview

Planet‑brand Ethernet switches require an interactive, PTY‑allocated SSH shell for authentication and CLI access. Their SSH server does not support non‑interactive exec channels for login or command execution.
Root Cause
The stock B4J SSHJ library (as distributed for B4J) only exposes non‑interactive exec‑style channels and does not provide a mechanism to open a PTY‑based interactive shell.
Planet switches rely on PTY allocation to present their username/password prompts and to maintain an authenticated session.
Failure Mode
When connecting without a PTY:
  • Planet still emits a Username: prompt
  • The switch rejects all submitted credentials
  • The login state machine resets
  • The session loops indefinitely with repeated Username: prompts
  • No CLI prompt (> or #) is ever reached
This behaviour is consistent across all Planet models that use the same SSH daemon.
Conclusion
Planet switches cannot be automated using the stock B4J SSHJ library because the library lacks PTY‑shell support, which Planet requires for interactive authentication and command execution.

===========================================

After a fruitless search for an existing solution I decided to have a crack at developing an SSH library with the assistance of Copilot.

This was a two-fold exercise - see what I could really do with Copilot and (if lucky) find a solution.

I FOUND IT!!!!!

EDIT - Check out the
Ferrari Version 1.1

EDIT 2 - Check out the enhanced error trapping Version 1.2

EDIT 3 - Check out the massively improved Version 3.0 by pitting Copilot against Claude

EDIT 4 - Check out the
fully self contained Version 3.1 (no external jsch-0.1.55.jar required) courtesy of Claude

EDIT 5 - Check out the
fully self contained Version 3.2 (no external jsch-0.1.55.jar required with fix for pixet)
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
CODE

I won't go thru the somewhat torturous conversations I had with Copilot but the net result was a folder in

N:\B4A\SimpleLibraryCompiler\B4JSSH with sub folders:

src
....B4JSSH.java
libs
....jsch-0.1.55.jar​

B4JSSH.java was entirely supplied by Copilot:
B4X:
package b4j.ssh;

import anywheresoftware.b4a.BA.ShortName;
import anywheresoftware.b4a.BA.Version;

import com.jcraft.jsch.*;

import java.io.InputStream;
import java.io.OutputStream;

@ShortName("B4JSSH")
@Version(1.00f)
public class B4JSSH {

    private JSch jsch;
    private Session session;
    private ChannelShell shell;
    private InputStream in;
    private OutputStream out;

    public void Initialize() {
        jsch = new JSch();
    }

    public void Connect(String host, int port, String user, String pass, int timeout) throws Exception {
        session = jsch.getSession(user, host, port);
        session.setPassword(pass);
        session.setConfig("StrictHostKeyChecking", "no");
        session.connect(timeout);
    }

    public void OpenShell() throws Exception {
        shell = (ChannelShell) session.openChannel("shell");
        shell.setPty(true);
        in = shell.getInputStream();
        out = shell.getOutputStream();
        shell.connect();
    }

    public void Write(String cmd) throws Exception {
        out.write((cmd + "\n").getBytes());
        out.flush();
    }

    public String Read() throws Exception {
        StringBuilder sb = new StringBuilder();
        while (in.available() > 0) {
            sb.append((char) in.read());
            Thread.sleep(10);
        }
        return sb.toString();
    }

    public void Disconnect() {
        try { if (shell != null) shell.disconnect(); } catch (Exception ignored) {}
        try { if (session != null) session.disconnect(); } catch (Exception ignored) {}
    }
}
The only real hassles with this was that initially the statement

@ShortName("B4JSSH")

was left out - eventually sorted by referring to an unrelated bit of java for another library that I had,

jsch-0.1.55.jar was sourced by Copilot at: https://mvnrepository.com/artifact/com.jcraft/jsch/0.1.55

and then the compilation steps (after a bit of confusion) were:

N:\B4A\SimpleLibraryCompiler > B4J_LibraryCompiler.exe
Java 8 Compiler: C:\Program Files\Eclipse Adoptium\jdk-8.0.472.8-hotspot\bin\javac.exe
Project Fo;der: N:\B4A\SimpleLibraryCompiler\B4JSSH
Library Name: B4JSSH
Compile

Builds: N:\B4J\Additional Libraries\B4JSSH.xml
N:\B4J\Additional Libraries\B4JSSH.jar

Then all that needed to be done was copy jsch-0.1.55.jar to N:\B4J\Additional Libraries and add this to my B4J project's Main module:

#Region Project Attributes
...
'For B4JSSH library:
#AdditionalJar: jsch-0.1.55.jar
...
#End Region

Then, obviously, make sure the B4JSSH library is ticked in the B4J project.

Sounds easy but took me a weekend...
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
INSTALLATION

Load the attached files into your B4J Additional Libraries folder.

Download jsch-0.1.55.jar from: https://mvnrepository.com/artifact/com.jcraft/jsch/0.1.55 and place it into your B4J Additional Libraries folder.

In your B4J project select the B4JSSH library and add this to the Main module:

#Region Project Attributes
...
'For B4JSSH library:
#AdditionalJar: jsch-0.1.55.jar
...
#End Region
 

Attachments

  • B4JSSH.jar
    1.4 KB · Views: 77
  • B4JSSH.xml
    1.9 KB · Views: 71
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
EXAMPLE

Here is a bit of the code from my new B4J wisp switch manager that shows how you can interact with Netonix, Planet and Ubiquiti switches:
B4X:
                    Try

                        'Always start with a fresh engine
                        Obj_ssh.Initialize

                        'Connect
                        Obj_ssh.Connect(wrk_switch_ip, wrk_switch_ssh_port, wrk_camera.switch_user_name, wrk_camera.switch_password, Gen_timeout * DateTime.TicksPerSecond)

                        'Open interactive PTY shell
                        Obj_ssh.OpenShell

                        'Flush camera's switch connect count
                        Gen_switch_connect_count.Remove(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port)

                    Catch
                  
                        'If camera's switch timeout count exists...
                        If Gen_switch_connect_count.ContainsKey(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port) Then
                      
                            'Increment it
                            Gen_switch_connect_count.Put(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port, NumberFormat(Gen_switch_connect_count.Get(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port), 1, 0) + 1)
                  
                        'Otherwise...
                        Else
                      
                            'Set it to 1
                            Gen_switch_connect_count.Put(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port, 1)
                  
                        End If
                  
                        Error_msg.Text = "Switch connect failure 1 " & Gen_switch_connect_count.Get(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port) & " of " & Gen_max_retries & " - " & wrk_camera.switch_type & ":" & wrk_switch_name & ":"  & wrk_switch_ip & ":" &  wrk_switch_ssh_port & "(" & NumberFormat(Gen_switch_connect_count.Get(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port), 1, 0) & ")"
                  
                        'Close failed session
                        Obj_ssh.Disconnect

                        'If camera's switch connection has failed Gen_max_retries times...
                        If Gen_max_retries = NumberFormat(Gen_switch_connect_count.Get(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port), 1, 0) Then
                  
                            'Save camera's switch error state
                            Gen_switch_error_state.Put(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port, Error_msg.Text)
                      
                            'Whatever reporting here
                      
                        End If
                  
                    End Try
              
                    'If switch connected...
                    If Not(Gen_switch_connect_count.ContainsKey(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port)) Then

                        'If Netonix switch...
                        If wrk_camera.switch_type = "Netonix" Then

                            'Ask switch for current configuration details
                            wrk_commands_sent = "terminal length 0" & CRLF & "show config" & CRLF & "exit"
                            Obj_ssh.Write(wrk_commands_sent)

                            'Read until we see Netonix prompt or timeout
                            wrk_result = SSH_Read_Until(Obj_ssh, wrk_switch_name & "# exit")

                            'If got Netonix prompt...
                            If wrk_result.Status = "Marker" Then
                      
                                'Extract JSON which is configuration details
                                wrk_json_start = wrk_result.Output.IndexOf("{")
                                wrk_json_end = wrk_result.Output.IndexOf(wrk_switch_name & "# exit")
                                wrk_json_str = wrk_result.Output.SubString2(wrk_json_start, wrk_json_end)

                                'If switch has not had its Revert Timer setting set to 0...
                                If wrk_json_str.IndexOf("""Switch_Revert_Timer"": ""0"",") = -1 Then
                  
                                    'Update Config log on disk
                                    Logger("Config", Gen_crlf & "Netonix switch non-zero revert timer - " & wrk_switch_type & ":"  & wrk_switch_name & ":" & wrk_switch_ip & ":" &  wrk_switch_ssh_port)
                      
                                    Error_msg.Text = "Netonix switch non-zero revert timer - " & wrk_switch_type & ":"  & wrk_switch_name & ":" & wrk_switch_ip & ":" &  wrk_switch_ssh_port
                          
                                    'As no timer is enabled this will cause app to hang
                                    Return
                      
                                End If
                  
                                'Extract PoE Ports configuration details
                                wrk_ports_start = wrk_json_str.IndexOf("""Ports"": [")
                                wrk_ports_end = wrk_json_str.IndexOf2("],", wrk_ports_start)
                                wrk_ports_str = wrk_json_str.SubString2(wrk_ports_start, wrk_ports_end)
                                wrk_port_number_end = 0
                  
                                wrk_port_poe.Clear

                                'Loop forever...
                                Do While True
              
                                    'Locate start of next PoE Port number field
                                    wrk_port_number_start = wrk_ports_str.IndexOf2("""Number"":", wrk_port_number_end) + 10
              
                                    'Quit loop if no more
                                    If wrk_port_number_start = 9 Then Exit
              
                                    'Locate end of PoE Port number field
                                    wrk_port_number_end = wrk_ports_str.IndexOf2(",", wrk_port_number_start)
              
                                    'Extract PoE Port number
                                    wrk_port_number = wrk_ports_str.SubString2(wrk_port_number_start, wrk_port_number_end)
              
                                    'Locate start of next PoE Port state field
                                    wrk_port_poe_start = wrk_ports_str.IndexOf2("""PoE"":", wrk_port_number_end) + 8
              
                                    'Locate end of PoE Port state field
                                    wrk_port_poe_end = wrk_ports_str.IndexOf2(""",", wrk_port_poe_start)
              
                                    'Extract PoE Port current state
                                    wrk_port_poe.Put(wrk_port_number, wrk_ports_str.SubString2(wrk_port_poe_start, wrk_port_poe_end))
                  
                                Loop
                  
                            'Otherwise, must have timed out...
                            Else
                  
                                Error_msg.Text = "Netonix switch show timeout - " & wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port
              
                                'Save error state of switch
                                Gen_switch_error_state.Put(wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port, Error_msg.Text)
                  
                                'Whatever reporting here

                            End If

                        'Otherwise, if Planet switch...
                        Else If wrk_camera.switch_type = "Planet" Then

                            'Read until we see Planet Username: prompt or timeout
                            wrk_result = SSH_Read_Until(Obj_ssh, "Username:")
                      
                            'If got Planet Username: prompt...
                            If wrk_result.Status = "Marker" Then
                      
                                'Send Planet switch username
                                wrk_commands_sent = wrk_camera.switch_user_name
                                Obj_ssh.Write(wrk_commands_sent)

                                'Read until we see Planet Password: prompt or timeout
                                wrk_result = SSH_Read_Until(Obj_ssh, "Password:")
                              
                                'If got Planet Password: prompt...
                                If wrk_result.Status = "Marker" Then
                                  
                                    'Send Planet switch password
                                    wrk_commands_sent = wrk_camera.switch_password
                                    Obj_ssh.Write(wrk_commands_sent)
                                  
                                    'Read until we see Planet # prompt or timeout
                                    wrk_result = SSH_Read_Until(Obj_ssh, "#")
                                  
                                    'If got Planet # prompt...
                                    If wrk_result.Status = "Marker" Then
                                  
                                        'Send Planet command to show PoE status of all PoE ports
                                        wrk_commands_sent = "show poe"
                                        Obj_ssh.Write(wrk_commands_sent)

                                        'Read until we see Planet prompt or timeout
                                        wrk_result = SSH_Read_Until(Obj_ssh, "#")

                                        'If got Planet # prompt...
                                        If wrk_result.Status = "Marker" Then

                                            'If got to here wrk_result.Output should look like this:
                                            '
                                            'PoE                     PD     Port               Power     Current
                                            'Interface               Class  Status             Used [W]  Used [mA]
                                            '----------------------  -----  -----------------  --------  ---------
                                            'GigabitEthernet 1/1     6      PoE ON             2 .9      62
                                            'GigabitEthernet 1/2     4      PoE ON             6 .1      129
                                            'GigabitEthernet 1/3     ---    PoE disabled       0 .0      0
                                            'GigabitEthernet 1/4     ---    PoE Search         0 .0      0
                                            'GigabitEthernet 1/5     ---    PoE Search         0 .0      0
                                            'GigabitEthernet 1/6     ---    PoE Search         0 .0      0
                                            'GigabitEthernet 1/7     ---    PoE Search         0 .0      0
                                            'GigabitEthernet 1/8     ---    PoE Search         0 .0      0
                                            '----------------------  -----  -----------------  --------  ---------
                                            '
                                            'Current Power Consumption    9. 0[W] (2%)
                                            'PoE Voltage                 47. 5[V]
                                            'PoE Version==> 3.55(  0)
                                            '
                                            'WGS-5225-8UP2SV#
                                            '
                                            'Notes 1. all Planet Wisp switches denote port numbers as 1/whatever
                                            '         with 1/ being common across all models e.g. "1/3" means port 3
                                            '      2. PD Class 6 = 802.3bt device (a.k.a. PoE++ e.g. Ubiquiti port 1)
                                            '      3. PD CLass 4 = 802.3at device (a.k.a. POE+ e.g. Hikvision camera)
                                            '      4. Normally only a Ubiquiti port 1 would be connected to any
                                            '         WGS-5225-8UP2SV port
                                            '      5. PoE ON = power on, automatically negotiated as per PD class
                                            '      6. PoE disabled = power off
                                            '      7. PoE Search = no device attached

                                            wrk_port_lines = Regex.Split(Gen_crlf, wrk_result.Output)
                          
                                            wrk_port_poe.Clear

                                            For Each wrk_port_line As String In wrk_port_lines

                                                'If a port line...
                                                If wrk_port_line.StartsWith("GigabitEthernet 1/") Then
                                  
                                                    'Strip out "GigabitEthernet 1/"
                                                    wrk_port_line = wrk_port_line.Replace("GigabitEthernet 1/", "")
                                  
                                                    'Extract PoE Port number
                                                    wrk_port_number = wrk_port_line.SubString2(0, 1)

                                                    'Extract PoE Port current state
                                                    If wrk_port_line.Contains("PoE ON") Then
                                                        wrk_port_poe.Put(wrk_port_number, "48V/H")
                                                    Else
                                                        wrk_port_poe.Put(wrk_port_number, "Off")
                                                    End If

                                                End If

                                            Next
                                      
                                        End If

                                    End If
                      
                                End If

                            End If

                            'If timed out somewhere above...
                            If wrk_result.Status = "Timeout" Then
              
                                Error_msg.Text = "Planet switch show timeout - " & wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port
          
                                'Save error state of switch
                                Gen_switch_error_state.Put(wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port, Error_msg.Text)
              
                                'Whatever reporting here

                            End If

                        'Otherwise, must be Ubiquiti switch...
                        Else

                            'Send Ubiquiti command to show PoE status of all ports
                            wrk_commands_sent = "ubntbox swctrl poe show" & CRLF & "exit"
                            Obj_ssh.Write(wrk_commands_sent)

                            'Read until we see marker or timeout
                            wrk_result = SSH_Read_Until(Obj_ssh, "# exit")

                            'If got marker...
                            If wrk_result.Status = "Marker" Then

                                'If got to here wrk_result.Output should look like this:
                                '
                                '~l04262279:
                                '~l04262279:
                                'BusyBox v1.25.1 () built-in shell (ash)
                                '~l04262279:
                                '~l04262279:
                                '  ___ ___      .__________.__
                                ' |   |   |____ |__\_  ____/__|
                                ' |   |   /    \|  ||  __) |  |   (c) 2010-2023
                                ' |   |  |   |  \  ||  \   |  |   Ubiquiti Inc.
                                ' |______|___|  /__||__/   |__|
                                '            |_/                  https://www.ui.com
                                '~l04262279:
                                '      Welcome to UniFi USW-Flex!
                                '~l04262279:
                                '********************************* NOTICE **********************************
                                '* By logging in to, accessing, or using any Ubiquiti product, you are     *
                                '* signifying that you have read our Terms of Service (ToS) and End User   *
                                '* License Agreement (EULA), understand their terms, and agree to be       *
                                '* fully bound to them. The use of SSH (Secure Shell) can potentially      *
                                '* harm Ubiquiti devices and result in lost access to them and their data. *
                                '* By proceeding, you acknowledge that the use of SSH to modify device(s)  *
                                '* outside of their normal operational scope, or in any manner             *
                                '* inconsistent with the ToS or EULA, will permanently and irrevocably     *
                                '* void any applicable warranty.                                           *
                                '***************************************************************************
                                '~l04262279:
                                'USW-Flex-US.6.5.32# Total Power Limit(mW): 46000
                                '~l04262279:
                                'Port  OpMode      HpMode    PwrLimit   Class   PoEPwr  PwrGood  Power(W)  Voltage(V)  Current(mA)
                                '                              (mW)
                                '----  ------  ------------  --------  -------  ------  -------  --------  ----------  -----------
                                '   2    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                                '   3    Auto        Dot3at     31507  Class 4      On     Good      2.01       48.60         0.00
                                '   4    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                                '   5    Auto        Dot3at     31507  Class 4      On     Good      2.15       48.81        44.00
                                'USW-Flex-US.6.5.32# exit
                              
                                'Extract block of configuration details
                                wrk_block_start = wrk_result.Output.IndexOf("----  ------  ------------")
                                wrk_block_start = wrk_result.Output.IndexOf2("   2", wrk_block_start)
                                wrk_block_end = wrk_result.Output.IndexOf("# exit")
                                wrk_block_end = wrk_result.Output.LastIndexOf2(Gen_crlf, wrk_block_end)
                                wrk_block_str = wrk_result.Output.SubString2(wrk_block_start, wrk_block_end + 2)

                                'wrk_block_str should look like this:
                                '
                                '   2    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                                '   3    Auto        Dot3at     31507  Class 4      On     Good      2.01       48.60         0.00
                                '   4    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                                '   5    Auto        Dot3at     31507  Class 4      On     Good      2.15       48.81        44.00

                                wrk_port_lines = Regex.Split(Gen_crlf, wrk_block_str)

                                wrk_port_poe.Clear

                                For Each wrk_port_line As String In wrk_port_lines

                                    'wrk_port_line should look like this:
                                    '
                                    '   2    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                      
                                    'Extract PoE Port number
                                    wrk_port_number = wrk_port_line.SubString2(0, 4)
                          
                                    'Extract PoE Port current state
                                    If wrk_port_line.IndexOf(" Off ") = -1 Then
                                        wrk_port_poe.Put(wrk_port_number, "48V")
                                    Else
                                        wrk_port_poe.Put(wrk_port_number, "Off")
                                    End If

                                Next

                            'Otherwise, must have timed out...
                            Else
                                                          
                                Error_msg.Text = "Ubiquiti switch show timeout - " & wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port
                              
                                'Save error state of switch
                                Gen_switch_error_state.Put(wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port, Error_msg.Text)
                                  
                                'Whatever reporting here
                          
                            End If

                        End If
                              
                        Obj_ssh.Disconnect
              
                    End If

                End If

and the SSH_Read_Until sub looks like this:
B4X:
Private Sub SSH_Read_Until(SSH_object As B4JSSH, Marker As String) As SSH_Read_Result_Type

    Private wrk_result As SSH_Read_Result_Type
    Private wrk_chunk, wrk_str As String
    Private wrk_end As Long = DateTime.Now + Gen_timeout * DateTime.tickspersecond

    Private wrk_jo As JavaObject
    wrk_jo.InitializeStatic("java.lang.Thread")

    Do While DateTime.Now  < wrk_end
        wrk_chunk = SSH_object.Read
        If wrk_chunk.Length > 0 Then
            wrk_str = wrk_str & wrk_chunk

            'Marker detection
            If wrk_str.Contains(Marker) Then
                wrk_result.Status = "Marker"
                wrk_result.Output = wrk_str
                Return wrk_result
            End If

        End If

        '5 ms pause, stays inside loop      
        Private wrk_5 As Long = 5
        wrk_jo.RunMethodJO("sleep", Array As Object(wrk_5))
      
    Loop

    'Iimeout detection
    wrk_result.Status = "Timeout"
    wrk_result.Output = wrk_str
    Return wrk_result
  
End Sub
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
USER DOCUMENTATION

For what it is worth here is some documentation I got Copilot to do:

 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
KNOWN BUGS
 

JackKirk

Well-Known Member
Licensed User
Longtime User
ADDITIONAL NOTES

I don't do java, xml or build libraries but Copilot can - with a lot of guidance in the form of progressively less dumb questions from me....
 
Last edited:

MicroDrie

Well-Known Member
Licensed User
Longtime User
Thank you for sharing this information.
 

JackKirk

Well-Known Member
Licensed User
Longtime User
Version 1.1 (Ferrari) - PREAMBLE

I don't know if anyone is actually interested in the functionality of this library but on the off chance...

I have done some extensive testing on the 1.0 version described above and found performance to be very ordinary - for example compared to Windows .bat files that I have had for some time that allows me to manually fiddle with Ubiquiti switches.

These .bat files use plink.

I queried Copilot on this and. after several goes around the merry-go-round it came up with a revised version.

This version has 2 optional boolean settings:

Obj_ssh.SetPTY
Obj_ssh.SetInitialNewline

and a revised Read method, which seems to be the major improvement.
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
Version 1.1 (Ferrari) - CODE

I have left the original up so anyone who is interested can see the changes made.

The new B4JSSH.java is:
B4X:
package b4j.ssh;

import anywheresoftware.b4a.BA.ShortName;
import anywheresoftware.b4a.BA.Version;

import com.jcraft.jsch.*;

import java.io.InputStream;
import java.io.OutputStream;

@ShortName("B4JSSH")
@Version(1.10f)
public class B4JSSH {

    private JSch jsch;
    private Session session;
    private ChannelShell shell;
    private InputStream in;
    private OutputStream out;

    //Defaults - match version 1.0 functionality
    private boolean usePTY = true;          
    private boolean sendInitialNewline = false;

    public void Initialize() {
        jsch = new JSch();
    }

    //Allow B4J to set PTY mode before opening shell
    public void SetPTY(boolean enabled) {
        this.usePTY = enabled;
    }

    //Allow B4J to toggle initial newline before opening shell
    public void SetInitialNewline(boolean enabled) {
        this.sendInitialNewline = enabled;
    }

    public void Connect(String host, int port, String user, String pass, int timeout) throws Exception {
        session = jsch.getSession(user, host, port);
        session.setPassword(pass);

        //Safe automation defaults
        session.setConfig("StrictHostKeyChecking", "no");
        session.setConfig("UserKnownHostsFile", "/dev/null");
        session.setConfig("PreferredAuthentications", "password");

        session.connect(timeout);
    }

    public void OpenShell() throws Exception {
 
        //Add a guard for session == null
        if (session == null || !session.isConnected()) {
            throw new IllegalStateException("Session not connected.");
        }
   
        shell = (ChannelShell) session.openChannel("shell");

        //Apply PTY mode chosen by user
        shell.setPty(usePTY);

        in = shell.getInputStream();
        out = shell.getOutputStream();
        shell.connect();

        //Some devices (Cisco, Juniper) need 20–40ms before sending data
        Thread.sleep(20);

        //Optional banner bypass
        if (sendInitialNewline) {

            //Adaptive banner detection - wait until remote side actually starts
            //sending data - this avoids guessing delays (50ms, 150ms, etc.)
            long start = System.currentTimeMillis();
            //2 seconds max wait for banner
            long timeout = 2000;

            while (System.currentTimeMillis() - start < timeout) {
                if (in.available() > 0) {
                    //Banner has begun arriving - safe to send newline
                    break;
                }
                Thread.sleep(10);
            }

            out.write("\n".getBytes());
            out.flush();
        }
    }

    public void Write(String cmd) throws Exception {
        out.write((cmd + "\n").getBytes());
        out.flush();
    }

    public String Read() throws Exception {
        StringBuilder sb = new StringBuilder();
        long start = System.currentTimeMillis();

        byte[] buffer = new byte[1024];

        //Active read loop for deterministic output
        while (System.currentTimeMillis() - start < 300) {
            //Non-blocking read - only read when data is available
            while (in.available() > 0) {
                int len = in.read(buffer);
                if (len > 0) {
                    sb.append(new String(buffer, 0, len, "UTF-8"));
                }
            }
            Thread.sleep(10);
        }
        return sb.toString();
    }

    public void Disconnect() {
        try { if (shell != null) shell.disconnect(); } catch (Exception ignored) {}
        try { if (session != null) session.disconnect(); } catch (Exception ignored) {}
    }
}
and then the compilation steps (as before) are:

N:\B4A\SimpleLibraryCompiler > B4J_LibraryCompiler.exe
Java 8 Compiler: C:\Program Files\Eclipse Adoptium\jdk-8.0.472.8-hotspot\bin\javac.exe
Project Fo;der: N:\B4A\SimpleLibraryCompiler\B4JSSH
Library Name: B4JSSH
Compile

Builds: N:\B4J\Additional Libraries\B4JSSH.xml
N:\B4J\Additional Libraries\B4JSSH.jar

- now 1.1

And as before, all that needs to be done is copy jsch-0.1.55.jar to N:\B4J\Additional Libraries and add this to the B4J project's Main module:

#Region Project Attributes
...
'For B4JSSH library:
#AdditionalJar: jsch-0.1.55.jar
...
#End Region

Then, obviously, make sure the B4JSSH library is ticked in the B4J project.
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
Version 1.1 (Ferrari) - INSTALLATION

Load the attached files into your B4J Additional Libraries folder.

Download jsch-0.1.55.jar from: https://mvnrepository.com/artifact/com.jcraft/jsch/0.1.55 and place it into your B4J Additional Libraries folder.

In your B4J project select the B4JSSH library (make sure it is 1.1) and add this to the Main module:

#Region Project Attributes
...
'For B4JSSH library:
#AdditionalJar: jsch-0.1.55.jar
...
#End Region
 

Attachments

  • B4JSSH.xml
    2.4 KB · Views: 63
  • B4JSSH.jar
    1.9 KB · Views: 58

JackKirk

Well-Known Member
Licensed User
Longtime User
Version 1.1 (Ferrari) - EXAMPLE

You could use the original example code - this would just use defaults for the 2 optional boolean settings:

Obj_ssh.SetPTY(True)
Obj_ssh.SetInitialNewline(False)

If you are using Netonix, Planet or Ubiquiti switches you might consider revising this code as follows:
B4X:
                    Try

                        'Always start with a fresh engine
                        Obj_ssh.Initialize

                        'If Netonix switch...
                        If wrk_camera.switch_type = "Netonix" Then

                            'No PTY needed (faster)
                            Obj_ssh.SetPTY(False)

                            'No banner .: no newline
                            Obj_ssh.SetInitialNewline(False)

                        'Otherwise, if Planet switch...
                        Else If wrk_camera.switch_type = "Planet" Then

                            'PTY required for prompt
                            Obj_ssh.SetPTY(True)

                            'Newline breaks login
                            Obj_ssh.SetInitialNewline(False)

                        'Otherwise, must be Ubiquiti switch...
                        Else

                            'PTY required for prompt
                            Obj_ssh.SetPTY(True)

                            'Newline skips banner
                            Obj_ssh.SetInitialNewline(True)

                        End If

                        'Connect
                        Obj_ssh.Connect(wrk_switch_ip, wrk_switch_ssh_port, wrk_camera.switch_user_name, wrk_camera.switch_password, Gen_timeout * DateTime.TicksPerSecond)

                        'Open interactive PTY shell
                        Obj_ssh.OpenShell

                        'Flush camera's switch connect count
                        Gen_switch_connect_count.Remove(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port)

                    Catch
                 
                        'If camera's switch timeout count exists...
                        If Gen_switch_connect_count.ContainsKey(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port) Then
                     
                            'Increment it
                            Gen_switch_connect_count.Put(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port, NumberFormat(Gen_switch_connect_count.Get(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port), 1, 0) + 1)
                 
                        'Otherwise...
                        Else
                     
                            'Set it to 1
                            Gen_switch_connect_count.Put(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port, 1)
                 
                        End If
                 
                        Error_msg.Text = "Switch connect failure 1 " & Gen_switch_connect_count.Get(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port) & " of " & Gen_max_retries & " - " & wrk_camera.switch_type & ":" & wrk_switch_name & ":"  & wrk_switch_ip & ":" &  wrk_switch_ssh_port & "(" & NumberFormat(Gen_switch_connect_count.Get(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port), 1, 0) & ")"
                 
                        'Close failed session
                        Obj_ssh.Disconnect

                        'If camera's switch connection has failed Gen_max_retries times...
                        If Gen_max_retries = NumberFormat(Gen_switch_connect_count.Get(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port), 1, 0) Then
                 
                            'Save camera's switch error state
                            Gen_switch_error_state.Put(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port, Error_msg.Text)
                     
                            'Whatever reporting here
                     
                        End If
                 
                    End Try
             
                    'If switch connected...
                    If Not(Gen_switch_connect_count.ContainsKey(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port)) Then

                        'If Netonix switch...
                        If wrk_camera.switch_type = "Netonix" Then

                            'Ask switch for current configuration details
                            wrk_commands_sent = "terminal length 0" & CRLF & "show config" & CRLF & "exit"
                            Obj_ssh.Write(wrk_commands_sent)

                            'Read until we see Netonix prompt or timeout
                            wrk_result = SSH_Read_Until(Obj_ssh, wrk_switch_name & "# exit")

                            'If got Netonix prompt...
                            If wrk_result.Status = "Marker" Then
                     
                                'Extract JSON which is configuration details
                                wrk_json_start = wrk_result.Output.IndexOf("{")
                                wrk_json_end = wrk_result.Output.IndexOf(wrk_switch_name & "# exit")
                                wrk_json_str = wrk_result.Output.SubString2(wrk_json_start, wrk_json_end)

                                'If switch has not had its Revert Timer setting set to 0...
                                If wrk_json_str.IndexOf("""Switch_Revert_Timer"": ""0"",") = -1 Then
                 
                                    'Update Config log on disk
                                    Logger("Config", Gen_crlf & "Netonix switch non-zero revert timer - " & wrk_switch_type & ":"  & wrk_switch_name & ":" & wrk_switch_ip & ":" &  wrk_switch_ssh_port)
                     
                                    Error_msg.Text = "Netonix switch non-zero revert timer - " & wrk_switch_type & ":"  & wrk_switch_name & ":" & wrk_switch_ip & ":" &  wrk_switch_ssh_port
                         
                                    'As no timer is enabled this will cause app to hang
                                    Return
                     
                                End If
                 
                                'Extract PoE Ports configuration details
                                wrk_ports_start = wrk_json_str.IndexOf("""Ports"": [")
                                wrk_ports_end = wrk_json_str.IndexOf2("],", wrk_ports_start)
                                wrk_ports_str = wrk_json_str.SubString2(wrk_ports_start, wrk_ports_end)
                                wrk_port_number_end = 0
                 
                                wrk_port_poe.Clear

                                'Loop forever...
                                Do While True
             
                                    'Locate start of next PoE Port number field
                                    wrk_port_number_start = wrk_ports_str.IndexOf2("""Number"":", wrk_port_number_end) + 10
             
                                    'Quit loop if no more
                                    If wrk_port_number_start = 9 Then Exit
             
                                    'Locate end of PoE Port number field
                                    wrk_port_number_end = wrk_ports_str.IndexOf2(",", wrk_port_number_start)
             
                                    'Extract PoE Port number
                                    wrk_port_number = wrk_ports_str.SubString2(wrk_port_number_start, wrk_port_number_end)
             
                                    'Locate start of next PoE Port state field
                                    wrk_port_poe_start = wrk_ports_str.IndexOf2("""PoE"":", wrk_port_number_end) + 8
             
                                    'Locate end of PoE Port state field
                                    wrk_port_poe_end = wrk_ports_str.IndexOf2(""",", wrk_port_poe_start)
             
                                    'Extract PoE Port current state
                                    wrk_port_poe.Put(wrk_port_number, wrk_ports_str.SubString2(wrk_port_poe_start, wrk_port_poe_end))
                 
                                Loop
                 
                            'Otherwise, must have timed out...
                            Else
                 
                                Error_msg.Text = "Netonix switch show timeout - " & wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port
             
                                'Save error state of switch
                                Gen_switch_error_state.Put(wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port, Error_msg.Text)
                 
                                'Whatever reporting here

                            End If

                        'Otherwise, if Planet switch...
                        Else If wrk_camera.switch_type = "Planet" Then

                            'Read until we see Planet Username: prompt or timeout
                            wrk_result = SSH_Read_Until(Obj_ssh, "Username:")
                     
                            'If got Planet Username: prompt...
                            If wrk_result.Status = "Marker" Then
                     
                                'Send Planet switch username
                                wrk_commands_sent = wrk_camera.switch_user_name
                                Obj_ssh.Write(wrk_commands_sent)

                                'Read until we see Planet Password: prompt or timeout
                                wrk_result = SSH_Read_Until(Obj_ssh, "Password:")
                             
                                'If got Planet Password: prompt...
                                If wrk_result.Status = "Marker" Then
                                 
                                    'Send Planet switch password
                                    wrk_commands_sent = wrk_camera.switch_password
                                    Obj_ssh.Write(wrk_commands_sent)
                                 
                                    'Read until we see Planet # prompt or timeout
                                    wrk_result = SSH_Read_Until(Obj_ssh, "#")
                                 
                                    'If got Planet # prompt...
                                    If wrk_result.Status = "Marker" Then
                                 
                                        'Send Planet command to show PoE status of all PoE ports
                                        wrk_commands_sent = "show poe"
                                        Obj_ssh.Write(wrk_commands_sent)

                                        'Read until we see Planet prompt or timeout
                                        wrk_result = SSH_Read_Until(Obj_ssh, "#")

                                        'If got Planet # prompt...
                                        If wrk_result.Status = "Marker" Then

                                            'If got to here wrk_result.Output should look like this:
                                            '
                                            'PoE                     PD     Port               Power     Current
                                            'Interface               Class  Status             Used [W]  Used [mA]
                                            '----------------------  -----  -----------------  --------  ---------
                                            'GigabitEthernet 1/1     6      PoE ON             2 .9      62
                                            'GigabitEthernet 1/2     4      PoE ON             6 .1      129
                                            'GigabitEthernet 1/3     ---    PoE disabled       0 .0      0
                                            'GigabitEthernet 1/4     ---    PoE Search         0 .0      0
                                            'GigabitEthernet 1/5     ---    PoE Search         0 .0      0
                                            'GigabitEthernet 1/6     ---    PoE Search         0 .0      0
                                            'GigabitEthernet 1/7     ---    PoE Search         0 .0      0
                                            'GigabitEthernet 1/8     ---    PoE Search         0 .0      0
                                            '----------------------  -----  -----------------  --------  ---------
                                            '
                                            'Current Power Consumption    9. 0[W] (2%)
                                            'PoE Voltage                 47. 5[V]
                                            'PoE Version==> 3.55(  0)
                                            '
                                            'WGS-5225-8UP2SV#
                                            '
                                            'Notes 1. all Planet Wisp switches denote port numbers as 1/whatever
                                            '         with 1/ being common across all models e.g. "1/3" means port 3
                                            '      2. PD Class 6 = 802.3bt device (a.k.a. PoE++ e.g. Ubiquiti port 1)
                                            '      3. PD CLass 4 = 802.3at device (a.k.a. POE+ e.g. Hikvision camera)
                                            '      4. Normally only a Ubiquiti port 1 would be connected to any
                                            '         WGS-5225-8UP2SV port
                                            '      5. PoE ON = power on, automatically negotiated as per PD class
                                            '      6. PoE disabled = power off
                                            '      7. PoE Search = no device attached

                                            wrk_port_lines = Regex.Split(Gen_crlf, wrk_result.Output)
                         
                                            wrk_port_poe.Clear

                                            For Each wrk_port_line As String In wrk_port_lines

                                                'If a port line...
                                                If wrk_port_line.StartsWith("GigabitEthernet 1/") Then
                                 
                                                    'Strip out "GigabitEthernet 1/"
                                                    wrk_port_line = wrk_port_line.Replace("GigabitEthernet 1/", "")
                                 
                                                    'Extract PoE Port number
                                                    wrk_port_number = wrk_port_line.SubString2(0, 1)

                                                    'Extract PoE Port current state
                                                    If wrk_port_line.Contains("PoE ON") Then
                                                        wrk_port_poe.Put(wrk_port_number, "48V/H")
                                                    Else
                                                        wrk_port_poe.Put(wrk_port_number, "Off")
                                                    End If

                                                End If

                                            Next
                                     
                                        End If

                                    End If
                     
                                End If

                            End If

                            'If timed out somewhere above...
                            If wrk_result.Status = "Timeout" Then
             
                                Error_msg.Text = "Planet switch show timeout - " & wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port
         
                                'Save error state of switch
                                Gen_switch_error_state.Put(wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port, Error_msg.Text)
             
                                'Whatever reporting here

                            End If

                        'Otherwise, must be Ubiquiti switch...
                        Else

                            'Send Ubiquiti command to show PoE status of all ports
                            wrk_commands_sent = "ubntbox swctrl poe show" & CRLF & "exit"
                            Obj_ssh.Write(wrk_commands_sent)

                            'Read until we see marker or timeout
                            wrk_result = SSH_Read_Until(Obj_ssh, "# exit")

                            'If got marker...
                            If wrk_result.Status = "Marker" Then

                                'If got to here wrk_result.Output should look like this:
                                '
                                '~l04262279:
                                '~l04262279:
                                'BusyBox v1.25.1 () built-in shell (ash)
                                '~l04262279:
                                '~l04262279:
                                '  ___ ___      .__________.__
                                ' |   |   |____ |__\_  ____/__|
                                ' |   |   /    \|  ||  __) |  |   (c) 2010-2023
                                ' |   |  |   |  \  ||  \   |  |   Ubiquiti Inc.
                                ' |______|___|  /__||__/   |__|
                                '            |_/                  https://www.ui.com
                                '~l04262279:
                                '      Welcome to UniFi USW-Flex!
                                '~l04262279:
                                '********************************* NOTICE **********************************
                                '* By logging in to, accessing, or using any Ubiquiti product, you are     *
                                '* signifying that you have read our Terms of Service (ToS) and End User   *
                                '* License Agreement (EULA), understand their terms, and agree to be       *
                                '* fully bound to them. The use of SSH (Secure Shell) can potentially      *
                                '* harm Ubiquiti devices and result in lost access to them and their data. *
                                '* By proceeding, you acknowledge that the use of SSH to modify device(s)  *
                                '* outside of their normal operational scope, or in any manner             *
                                '* inconsistent with the ToS or EULA, will permanently and irrevocably     *
                                '* void any applicable warranty.                                           *
                                '***************************************************************************
                                '~l04262279:
                                'USW-Flex-US.6.5.32# Total Power Limit(mW): 46000
                                '~l04262279:
                                'Port  OpMode      HpMode    PwrLimit   Class   PoEPwr  PwrGood  Power(W)  Voltage(V)  Current(mA)
                                '                              (mW)
                                '----  ------  ------------  --------  -------  ------  -------  --------  ----------  -----------
                                '   2    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                                '   3    Auto        Dot3at     31507  Class 4      On     Good      2.01       48.60         0.00
                                '   4    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                                '   5    Auto        Dot3at     31507  Class 4      On     Good      2.15       48.81        44.00
                                'USW-Flex-US.6.5.32# exit
                             
                                'Extract block of configuration details
                                wrk_block_start = wrk_result.Output.IndexOf("----  ------  ------------")
                                wrk_block_start = wrk_result.Output.IndexOf2("   2", wrk_block_start)
                                wrk_block_end = wrk_result.Output.IndexOf("# exit")
                                wrk_block_end = wrk_result.Output.LastIndexOf2(Gen_crlf, wrk_block_end)
                                wrk_block_str = wrk_result.Output.SubString2(wrk_block_start, wrk_block_end + 2)

                                'wrk_block_str should look like this:
                                '
                                '   2    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                                '   3    Auto        Dot3at     31507  Class 4      On     Good      2.01       48.60         0.00
                                '   4    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                                '   5    Auto        Dot3at     31507  Class 4      On     Good      2.15       48.81        44.00

                                wrk_port_lines = Regex.Split(Gen_crlf, wrk_block_str)

                                wrk_port_poe.Clear

                                For Each wrk_port_line As String In wrk_port_lines

                                    'wrk_port_line should look like this:
                                    '
                                    '   2    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                     
                                    'Extract PoE Port number
                                    wrk_port_number = wrk_port_line.SubString2(0, 4)
                         
                                    'Extract PoE Port current state
                                    If wrk_port_line.IndexOf(" Off ") = -1 Then
                                        wrk_port_poe.Put(wrk_port_number, "48V")
                                    Else
                                        wrk_port_poe.Put(wrk_port_number, "Off")
                                    End If

                                Next

                            'Otherwise, must have timed out...
                            Else
                                                         
                                Error_msg.Text = "Ubiquiti switch show timeout - " & wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port
                             
                                'Save error state of switch
                                Gen_switch_error_state.Put(wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port, Error_msg.Text)
                                 
                                'Whatever reporting here
                         
                            End If

                        End If
                             
                        Obj_ssh.Disconnect
             
                    End If

                End If
the only change is:
B4X:
                        'If Netonix switch...
                        If wrk_camera.switch_type = "Netonix" Then

                            'No PTY needed (faster)
                            Obj_ssh.SetPTY(False)

                            'No banner .: no newline
                            Obj_ssh.SetInitialNewline(False)

                        'Otherwise, if Planet switch...
                        Else If wrk_camera.switch_type = "Planet" Then

                            'PTY required for prompt
                            Obj_ssh.SetPTY(True)

                            'Newline breaks login
                            Obj_ssh.SetInitialNewline(False)

                        'Otherwise, must be Ubiquiti switch...
                        Else

                            'PTY required for prompt
                            Obj_ssh.SetPTY(True)

                            'Newline skips banner
                            Obj_ssh.SetInitialNewline(True)

                        End If
placed between the Initialize and Connect

These settings are as recommended by Copilot for each switch and have tested OK.

If you were using a different switch (Cisco, ...) you might need to experiment (there are only 4 possible combinations).

The SSH_Read_Until sub is as before.
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
Version 1.1 (Ferrari) - DOCUMENTATION

 

JackKirk

Well-Known Member
Licensed User
Longtime User
Version 1.1 (Ferrari) - ADDITIONAL NOTES

I believe this exercise demonstrates the strong need for guidance of MLs like Copilot - I (by default) assumed that it would give me the most refined solution possible when I originally queried it on the subject.

But, in hindsight, it initially only gave the bare minimum that worked.

It was only after I badgered it for a better performing version that it delivered.

Which suggests the human element is still required for quality results (for now at least).

That said, I am really quite chuffed that I now have the ability to do things like this that are way outside my historic knowledge envelope.
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
Version 1.2 (Enhanced error checking) - PREAMBLE/CODE/INSTALLATION/EXAMPLE/DOCUMENTATION/ADDITIONAL NOTES

PREAMBLE

As I said above:
I believe this exercise demonstrates the strong need for guidance of MLs like Copilot - I (by default) assumed that it would give me the most refined solution possible when I originally queried it on the subject.
It turns out that the earlier versions did not have any serious error checking around connecting - and again MS Copilot had to be pushed to provide it.

CODE
The new B4JSSH.java is:
B4X:
package b4j.ssh;

import anywheresoftware.b4a.BA.ShortName;
import anywheresoftware.b4a.BA.Version;

import com.jcraft.jsch.*;

import java.io.InputStream;
import java.io.OutputStream;

@ShortName("B4JSSH")
@Version(1.20f)
public class B4JSSH {

    private JSch jsch;
    private Session session;
    private ChannelShell shell;
    private InputStream in;
    private OutputStream out;

    //Defaults - match version 1.0 functionality
    private boolean usePTY = true;            
    private boolean sendInitialNewline = false;

    public void Initialize() {
        jsch = new JSch();
    }

    //Allow B4J to set PTY mode before opening shell
    public void SetPTY(boolean enabled) {
        this.usePTY = enabled;
    }

    //Allow B4J to toggle initial newline before opening shell
    public void SetInitialNewline(boolean enabled) {
        this.sendInitialNewline = enabled;
    }

    public void Connect(String host, int port, String user, String pass, int timeout) throws Exception {
        jsch = new JSch();
        session = jsch.getSession(user, host, port);
        session.setPassword(pass);

        //Safe automation defaults
        session.setConfig("StrictHostKeyChecking", "no");
        session.setConfig("UserKnownHostsFile", "/dev/null");
        session.setConfig("PreferredAuthentications", "password");

        //Attempt TCP + SSH handshake ---
        try {
            session.connect(timeout);
        } catch (Exception ex) {
            throw new IllegalStateException("SSH connection failed: " + ex.getMessage(), ex);
        }

        //Post-connect validation: ensure SSH negotiation completed ---
        if (!session.isConnected()) {
            throw new IllegalStateException("SSH session is not connected after connect().");
        }

        //Server banner must be present (Planet sometimes fails here)
        String serverVersion = session.getServerVersion();
        if (serverVersion == null || serverVersion.trim().isEmpty()) {
            session.disconnect();
            throw new IllegalStateException("SSH negotiation failed: no server banner received.");
        }

        //Authentication must be complete
        if (!session.isConnected()) {
            session.disconnect();
            throw new IllegalStateException("SSH authentication did not complete.");
        }
    }

    public void OpenShell() throws Exception {
 
        //Add a guard for session == null
        if (session == null || !session.isConnected()) {
            throw new IllegalStateException("Session not connected.");
        }
     
        shell = (ChannelShell) session.openChannel("shell");

        //Apply PTY mode chosen by user
        shell.setPty(usePTY);

        in = shell.getInputStream();
        out = shell.getOutputStream();
        shell.connect();

        //Some devices (Cisco, Juniper) need 20–40ms before sending data
        Thread.sleep(20);

        //Optional banner bypass
        if (sendInitialNewline) {

            //Adaptive banner detection - wait until remote side actually starts
            //sending data - this avoids guessing delays (50ms, 150ms, etc.)
            long start = System.currentTimeMillis();
            //2 seconds max wait for banner
            long timeout = 2000;

            while (System.currentTimeMillis() - start < timeout) {
                if (in.available() > 0) {
                    //Banner has begun arriving - safe to send newline
                    break;
                }
                Thread.sleep(10);
            }

            out.write("\n".getBytes());
            out.flush();
        }
    }

    public void Write(String cmd) throws Exception {
        out.write((cmd + "\n").getBytes());
        out.flush();
    }

    public String Read() throws Exception {
        StringBuilder sb = new StringBuilder();
        long start = System.currentTimeMillis();

        byte[] buffer = new byte[1024];

        //Active read loop for deterministic output
        while (System.currentTimeMillis() - start < 300) {
            //Non-blocking read - only read when data is available
            while (in.available() > 0) {
                int len = in.read(buffer);
                if (len > 0) {
                    sb.append(new String(buffer, 0, len, "UTF-8"));
                }
            }
            Thread.sleep(10);
        }
        return sb.toString();
    }

    public void Disconnect() {
        try { if (shell != null) shell.disconnect(); } catch (Exception ignored) {}
        try { if (session != null) session.disconnect(); } catch (Exception ignored) {}
    }
}

INSTALLATION
Load the attached files into your B4J Additional Libraries folder.

Download jsch-0.1.55.jar from: https://mvnrepository.com/artifact/com.jcraft/jsch/0.1.55 and place it into your B4J Additional Libraries folder.

In your B4J project select the B4JSSH library (make sure it is 1.2) and add this to the Main module:

#Region Project Attributes
...
'For B4JSSH library:
#AdditionalJar: jsch-0.1.55.jar
...
#End Region

EXAMPLE/DOCUMENTATION
Example code and user documentation the same as the most recent efforts above,

ADDITIONAL NOTES
-
 

Attachments

  • B4JSSH.jar
    2.2 KB · Views: 49
  • B4JSSH.xml
    2.4 KB · Views: 55
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
Version 3.0 (Massively improved via pitting Copilot against Claude) - PREAMBLE/CODE/INSTALLATION/EXAMPLE/DOCUMENTATION/ADDITIONAL NOTES

PREAMBLE

After the previous post revealed how minimalistic Copilot's efforts were without serious prompting I decided to push it hard.

The exact mechanism I have detailed in this post because I suspect the lesson I have learnt is worth broadcasting far and wide.

CODE
The new B4JSSH.java is:
B4X:
package b4j.ssh;

import anywheresoftware.b4a.BA.ShortName;
import anywheresoftware.b4a.BA.Version;

import com.jcraft.jsch.*;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Minimal deterministic SSH shell wrapper for B4J.
 * Provides a simple, safe, predictable API for interactive SSH automation.
 */
@ShortName("B4JSSH")
@Version(3.0f)
public class B4JSSH {

    // -------------------------------------------------------------------------
    // Fields
    // -------------------------------------------------------------------------

    private JSch jsch;
    private Session session;
    private ChannelShell shell;
    private InputStream in;
    private OutputStream out;

    private final LinkedBlockingDeque<Chunk> queue = new LinkedBlockingDeque<>();

    private final byte[] readerBuf = new byte[4096];
    private final CharsetDecoder decoder =
        StandardCharsets.UTF_8.newDecoder()
            .onMalformedInput(CodingErrorAction.REPORT)
            .onUnmappableCharacter(CodingErrorAction.REPORT);
    private final ByteBuffer decodeBytes = ByteBuffer.allocate(4096 + 3);
    private final CharBuffer decodeChars = CharBuffer.allocate(8192);

    private Thread readerThread;

    private final AtomicLong generation = new AtomicLong(0);

    private static final class Chunk {
        final String data;
        final boolean eof;
        final long gen;

        Chunk(String d, boolean eof, long gen) {
            this.data = d;
            this.eof = eof;
            this.gen = gen;
        }

        static Chunk data(String s, long g) { return new Chunk(s, false, g); }
        static Chunk eof(long g)           { return new Chunk(null, true, g); }
    }

    private static final int INTER_CHUNK_IDLE_MS = 150;

    // -------------------------------------------------------------------------
    // Lifecycle
    // -------------------------------------------------------------------------

    public void Initialize() {
        if (jsch != null)
            throw new SshException(SshException.ErrorKind.INTERNAL,
                "Initialize() called more than once.");
        jsch = new JSch();
    }

    public void Connect(String host, int port, String user, String pass, int timeoutMs) {
        Objects.requireNonNull(host, "host");
        Objects.requireNonNull(user, "user");
        Objects.requireNonNull(pass, "pass");

        if (port <= 0 || port > 65535)
            throw new SshException(SshException.ErrorKind.INTERNAL,
                "Invalid SSH port: " + port);
        if (timeoutMs <= 0)
            throw new SshException(SshException.ErrorKind.INTERNAL,
                "timeoutMs must be > 0. Value=" + timeoutMs);

        if (jsch == null)
            throw new SshException(SshException.ErrorKind.INTERNAL,
                "Connect() called before Initialize().");

        try {
            session = jsch.getSession(user, host, port);
            session.setPassword(pass);

            Properties cfg = new Properties();
            cfg.put("StrictHostKeyChecking", "no");
            cfg.put("PreferredAuthentications", "password");
            session.setConfig(cfg);

            session.connect(timeoutMs);

        } catch (JSchException ex) {
            safeDisconnect();
            throw new SshException(SshException.ErrorKind.CONNECT,
                "SSH connection failed: " + ex.getMessage(), ex);
        }
    }

    public void OpenShell() {
        if (session == null || !session.isConnected())
            throw new SshException(SshException.ErrorKind.INTERNAL,
                "OpenShell() called when session is not connected.");

        try {
            Channel ch = session.openChannel("shell");

            if (!(ch instanceof ChannelShell)) {
                ch.disconnect();
                // safeCloseShell() not needed: shell was never assigned
                throw new SshException(SshException.ErrorKind.PROTOCOL,
                    "Expected ChannelShell but got: " + ch.getClass().getName());
            }

            shell = (ChannelShell) ch;
            shell.setPty(true);

            in  = shell.getInputStream();
            out = shell.getOutputStream();

            shell.connect();

        } catch (JSchException | IOException ex) {
            safeCloseShell();
            throw new SshException(SshException.ErrorKind.PROTOCOL,
                "Failed to open shell: " + ex.getMessage(), ex);
        }

        queue.clear();
        long myGen = generation.incrementAndGet();
        startReader(myGen);
    }

    public void Disconnect() {
        safeDisconnect();
    }

    // -------------------------------------------------------------------------
    // I/O
    // -------------------------------------------------------------------------

    public void Write(String cmd) {
        Objects.requireNonNull(cmd, "cmd");
        ensureShell();
        try {
            out.write(cmd.getBytes(StandardCharsets.UTF_8));
            out.write('\n');
            out.flush();
        } catch (IOException ex) {
            throw new SshException(SshException.ErrorKind.INTERNAL,
                "Write() I/O error: " + ex.getMessage(), ex);
        }
    }

    public void WriteRaw(String data) {
        Objects.requireNonNull(data, "data");
        ensureShell();
        try {
            out.write(data.getBytes(StandardCharsets.UTF_8));
            out.flush();
        } catch (IOException ex) {
            throw new SshException(SshException.ErrorKind.INTERNAL,
                "WriteRaw() I/O error: " + ex.getMessage(), ex);
        }
    }

    public String ReadWindow(int timeoutMs) {
        ensureShell();
        if (timeoutMs <= 0)
            throw new SshException(SshException.ErrorKind.INTERNAL,
                "timeoutMs must be > 0. Value=" + timeoutMs);

        StringBuilder sb = new StringBuilder(512);
        long myGen = generation.get();
        long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs);
        boolean gotAny = false;

        while (true) {
            long now = System.nanoTime();
            if (now >= deadline)
                return sb.toString();

            long remMs = TimeUnit.NANOSECONDS.toMillis(deadline - now);
            long pollMs = gotAny ? Math.min(INTER_CHUNK_IDLE_MS, remMs) : remMs;
            if (pollMs < 1) pollMs = 1;

            Chunk c;
            try {
                c = queue.poll(pollMs, TimeUnit.MILLISECONDS);
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
                throw new SshException(SshException.ErrorKind.TIMEOUT,
                    "ReadWindow interrupted. Buffer: [" + sb + "]", ie);
            }

            if (c == null) {
                return sb.toString();
            }

            if (c.gen != myGen) {
                continue;
            }

            if (c.eof)
                throw new SshException(SshException.ErrorKind.REMOTE_CLOSED,
                    "Remote closed during ReadWindow. Buffer: [" + sb + "]");

            sb.append(c.data);
            gotAny = true;
        }
    }

    public String ReadUntil(String[] prompts, int timeoutMs) {
        ensureShell();

        if (prompts == null || prompts.length == 0)
            throw new SshException(SshException.ErrorKind.INTERNAL,
                "prompts must not be null or empty.");
        if (timeoutMs <= 0)
            throw new SshException(SshException.ErrorKind.INTERNAL,
                "timeoutMs must be > 0. Value=" + timeoutMs);

        List<String> list = new ArrayList<>();
        int maxLen = 0;

        for (String p : prompts) {
            if (p == null || p.isEmpty())
                throw new SshException(SshException.ErrorKind.INTERNAL,
                    "prompts must not contain null or empty strings.");
            list.add(p);
            if (p.length() > maxLen) maxLen = p.length();
        }

        StringBuilder sb = new StringBuilder(512);
        long myGen = generation.get();
        long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs);

        while (true) {
            long now = System.nanoTime();
            if (now >= deadline) {
                throw new SshException(SshException.ErrorKind.TIMEOUT,
                    "ReadUntil timed out. Buffer: [" + sb + "]");
            }

            long remMs = TimeUnit.NANOSECONDS.toMillis(deadline - now);
            if (remMs < 1) remMs = 1;

            Chunk c;
            try {
                c = queue.poll(remMs, TimeUnit.MILLISECONDS);
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
                throw new SshException(SshException.ErrorKind.TIMEOUT,
                    "ReadUntil interrupted. Buffer: [" + sb + "]", ie);
            }

            if (c == null) {
                throw new SshException(SshException.ErrorKind.TIMEOUT,
                    "ReadUntil timed out. Buffer: [" + sb + "]");
            }

            if (c.gen != myGen) {
                continue;
            }

            if (c.eof) {
                throw new SshException(SshException.ErrorKind.REMOTE_CLOSED,
                    "Remote closed during ReadUntil. Buffer: [" + sb + "]");
            }

            int prev = sb.length();
            sb.append(c.data);

            int searchFrom = Math.max(0, prev - (maxLen - 1));
            for (String p : list) {
                if (sb.indexOf(p, searchFrom) >= 0)
                    return sb.toString();
            }
        }
    }

    // -------------------------------------------------------------------------
    // State
    // -------------------------------------------------------------------------

    public boolean IsConnected() {
        return session != null && session.isConnected();
    }

    public boolean IsShellOpen() {
        return shell != null && shell.isConnected();
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    private void ensureShell() {
        if (shell == null || !shell.isConnected())
            throw new SshException(SshException.ErrorKind.INTERNAL,
                "Shell not open. Call OpenShell() first.");
    }

    private void safeCloseShell() {
        try {
            if (shell != null)
                shell.disconnect();
        } catch (Exception ignored) {
        } finally {
            shell = null;
        }

        try {
            if (in != null) in.close();
        } catch (Exception ignored) {}
        in = null;

        out = null;

        queue.clear();
        readerThread = null;
    }

    private void safeDisconnect() {
        safeCloseShell();

        try {
            if (session != null)
                session.disconnect();
        } catch (Exception ignored) {
        } finally {
            session = null;
        }
    }

    private void startReader(long myGen) {
        readerThread = new Thread(() -> {
            while (true) {
                int len;
                try {
                    len = in.read(readerBuf);
                } catch (IOException e) {
                    queue.offer(Chunk.eof(myGen));
                    break;
                }

                if (len < 0) {
                    queue.offer(Chunk.eof(myGen));
                    break;
                }

                try {
                    String s = decodeUtf8(readerBuf, 0, len);
                    if (!s.isEmpty())
                        queue.offer(Chunk.data(s, myGen));
                } catch (Exception ex) {
                    queue.offer(Chunk.eof(myGen));
                    break;
                }
            }
        }, "B4JSSH-Reader");

        readerThread.setDaemon(true);
        readerThread.start();
    }

    private String decodeUtf8(byte[] buf, int off, int len) {
        if (len > decodeBytes.remaining()) {
            throw new SshException(SshException.ErrorKind.INTERNAL,
                "UTF-8 decode overflow: len=" + len +
                " remaining=" + decodeBytes.remaining());
        }

        decodeBytes.put(buf, off, len);
        decodeBytes.flip();

        StringBuilder sb = new StringBuilder(len);

        while (true) {
            CoderResult r = decoder.decode(decodeBytes, decodeChars, false);
            if (r.isError())
                throw new SshException(SshException.ErrorKind.INTERNAL,
                    "Invalid UTF-8 sequence received from remote.");

            decodeChars.flip();
            if (decodeChars.hasRemaining())
                sb.append(decodeChars);
            decodeChars.clear();

            if (r.isUnderflow())
                break;
        }

        decodeBytes.compact();
        return sb.toString();
    }
}
This B4JSSH.java now comprises 413 lines vs 144 in the last version - some measure of the primitiveness of the last version.

and there is now a second bit of Java (SshException):
B4X:
package b4j.ssh;

import anywheresoftware.b4a.BA.ShortName;

/**
 * Unified SSH exception exposed to B4X.
 * Thrown by all B4JSSH operations.
 */
@ShortName("B4JSSH_SshException")
public class SshException extends RuntimeException {

    /** Enum describing the category of SSH error. */
    public enum ErrorKind {
        CONNECT,
        TIMEOUT,
        REMOTE_CLOSED,
        PROTOCOL,
        INTERNAL
    }

    /** B4X-visible error kind (capital K for consistency with Message). */
    public final ErrorKind Kind;

    public SshException(ErrorKind kind, String msg) {
        super(msg);
        this.Kind = kind;
    }

    public SshException(ErrorKind kind, String msg, Throwable cause) {
        super(msg, cause);
        this.Kind = kind;
    }
}

INSTALLATION
Load the attached files into your B4J Additional Libraries folder.

Download jsch-0.1.55.jar from: https://mvnrepository.com/artifact/com.jcraft/jsch/0.1.55 and place it into your B4J Additional Libraries folder.

In your B4J project select the B4JSSH library (make sure it is 3.0) and add this to the Main module:

#Region Project Attributes
...
'For B4JSSH library:
#AdditionalJar: jsch-0.1.55.jar
...
#End Region

EXAMPLE
Along the way I managed to get a "minimalistic" model working, with the example code significantly simpler:

B4X:
                    Try
                 
                        'Connect
                        Obj_ssh.Connect(wrk_switch_ip, wrk_switch_ssh_port, wrk_camera.switch_user_name, wrk_camera.switch_password, Gen_switch_contact_timeout)

                        'Open interactive shell
                        Obj_ssh.OpenShell

                    Catch
                 
'                        'Just do a normal LastException log
'                        Log(LastException)
'                
'                        'Alternatively decompose LastException
'                        Private wrk_exception As B4JSSH_SshException = LastException
'                        'CONNECT / PROTOCOL / TIMEOUT / REMOTE_CLOSED / INTERNAL                        'Flag camera switch error
'                        Log("SSH error kind: " & wrk_exception.Kind)
'                        Log("SSH error message: " & wrk_exception.Message)
                 
                        Gen_cameras_current_poe_state.Put(wrk_ptr, "???")
                 
                        'Close failed session
                        Obj_ssh.Disconnect
                     
                        'Stop processing current camera and move to next one
                        Continue
                 
                    End Try
                 
                    wrk_port_poe.Clear

                    'If Netonix switch...
                    If wrk_camera.switch_type = "Netonix" Then
'Log("Netonix show config")
                        'Ask switch for current configuration details
                        wrk_commands_sent = "terminal length 0" & CRLF & "show config" & CRLF & "exit"
                        Obj_ssh.Write(wrk_commands_sent)

                        'Read until we see Netonix prompt or failure (timeout?)
                        wrk_result = SSH_Read_Until(Obj_ssh, wrk_switch_name & "# exit")

'                        Log("ssh_Status ========================================")
'                        Log(wrk_result.Status)
'                        Log("ssh_Shell_commands_executed ========================================")
'                        Log(wrk_commands_sent)
'                        Log("ssh_Output.length ========================================")
'                        Log(wrk_result.Output.length)
'                        Log("ssh_Output ========================================")
'                        Log(wrk_result.Output)
'                        Log("========================================")
'                                
'                        Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " =============================== show config ================================")
'                        Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " ssh Status: " & wrk_result.status)
'                        Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " ssh Shell commands executed: " & wrk_commands_sent)
'                        Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " ssh Output Length: " & wrk_result.Output.length)
'                        Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " ssh Output: " & wrk_result.Output)
'                        Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " ============================================================================")
                     
                        'If got Netonix prompt...
                        If wrk_result.Status = "Marker" Then
                 
                            'Extract JSON which is configuration details
                            wrk_json_start = wrk_result.Output.IndexOf("{")
                            wrk_json_end = wrk_result.Output.IndexOf(wrk_switch_name & "# exit")
                            wrk_json_str = wrk_result.Output.SubString2(wrk_json_start, wrk_json_end)

                            'If switch has not had its Revert Timer setting set to 0...
                            If wrk_json_str.IndexOf("""Switch_Revert_Timer"": ""0"",") = -1 Then
             
                                'Update Config log on disk
                                Logger("Config", Gen_crlf & "Netonix switch non-zero revert timer - " & wrk_switch_type & ":"  & wrk_switch_name & ":" & wrk_switch_ip & ":" &  wrk_switch_ssh_port)
                 
                                Error_msg.Text = "Netonix switch non-zero revert timer - " & wrk_switch_type & ":"  & wrk_switch_name & ":" & wrk_switch_ip & ":" &  wrk_switch_ssh_port
                     
                                'As no timer is enabled this will cause app to hang
                                Return
                 
                            End If
             
                            'Extract PoE Ports configuration details
                            wrk_ports_start = wrk_json_str.IndexOf("""Ports"": [")
                            wrk_ports_end = wrk_json_str.IndexOf2("],", wrk_ports_start)
                            wrk_ports_str = wrk_json_str.SubString2(wrk_ports_start, wrk_ports_end)
                            wrk_port_number_end = 0

                            'Loop forever...
                            Do While True
         
                                'Locate start of next PoE Port number field
                                wrk_port_number_start = wrk_ports_str.IndexOf2("""Number"":", wrk_port_number_end) + 10
         
                                'Quit loop if no more
                                If wrk_port_number_start = 9 Then Exit
         
                                'Locate end of PoE Port number field
                                wrk_port_number_end = wrk_ports_str.IndexOf2(",", wrk_port_number_start)
         
                                'Extract PoE Port number
                                wrk_port_number = wrk_ports_str.SubString2(wrk_port_number_start, wrk_port_number_end)
         
                                'Locate start of next PoE Port state field
                                wrk_port_poe_start = wrk_ports_str.IndexOf2("""PoE"":", wrk_port_number_end) + 8
         
                                'Locate end of PoE Port state field
                                wrk_port_poe_end = wrk_ports_str.IndexOf2(""",", wrk_port_poe_start)
         
                                'Extract PoE Port current state
                                wrk_port_poe.Put(wrk_port_number, wrk_ports_str.SubString2(wrk_port_poe_start, wrk_port_poe_end))
             
                            Loop
             
                        'Otherwise, must have failed (timeout?)...
                        Else
             
                            'Flag camera switch error
                            Gen_cameras_current_poe_state.Put(wrk_ptr, "???")

                        End If

                    'Otherwise, if Planet switch...
                    Else If wrk_camera.switch_type = "Planet" Then
'Log("Planet show config")
                        'Read until we see Planet Username: prompt or failure (timeout?)
                        wrk_result = SSH_Read_Until(Obj_ssh, "Username:")
                 
                        'If got Planet Username: prompt...
                        If wrk_result.Status = "Marker" Then
                 
                            'Send Planet switch username
                            wrk_commands_sent = wrk_camera.switch_user_name
                            Obj_ssh.Write(wrk_commands_sent)

                            'Read until we see Planet Password: prompt or failure (timeout?)
                            wrk_result = SSH_Read_Until(Obj_ssh, "Password:")
                         
                            'If got Planet Password: prompt...
                            If wrk_result.Status = "Marker" Then
                             
                                'Send Planet switch password
                                wrk_commands_sent = wrk_camera.switch_password
                                Obj_ssh.Write(wrk_commands_sent)
                             
                                'Read until we see Planet # prompt or failure (timeout?)
                                wrk_result = SSH_Read_Until(Obj_ssh, "#")
                             
                                'If got Planet # prompt...
                                If wrk_result.Status = "Marker" Then
                             
                                    'Send Planet command to show PoE status of all PoE ports
                                    wrk_commands_sent = "show poe"
                                    Obj_ssh.Write(wrk_commands_sent)

                                    'Read until we see Planet prompt or failure (timeout?)
                                    wrk_result = SSH_Read_Until(Obj_ssh, "#")

'                                    Log("ssh_Status ========================================")
'                                    Log(wrk_result.Status)
'                                    Log("ssh_Shell_commands_executed ========================================")
'                                    Log(wrk_commands_sent)
'                                    Log("ssh_Output.length ========================================")
'                                    Log(wrk_result.Output.length)
'                                    Log("ssh_Output ========================================")
'                                    Log(wrk_result.Output)
'                                    Log("========================================")
'                                            
'                                    Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " =============================== show config ================================")
'                                    Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " ssh Success: " & wrk_result.Status)
'                                    Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " ssh Shell commands executed: " & wrk_commands_sent)
'                                    Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " ssh Output Length: " & wrk_result.Output.length)
'                                    Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " ssh Output: " & wrk_result.Output)
'                                    Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " ============================================================================")

                                    'If got Planet # prompt...
                                    If wrk_result.Status = "Marker" Then

                                        'If got to here wrk_result.Output should look like this:
                                        '
                                        'PoE                     PD     Port               Power     Current
                                        'Interface               Class  Status             Used [W]  Used [mA]
                                        '----------------------  -----  -----------------  --------  ---------
                                        'GigabitEthernet 1/1     6      PoE ON             2 .9      62
                                        'GigabitEthernet 1/2     4      PoE ON             6 .1      129
                                        'GigabitEthernet 1/3     ---    PoE disabled       0 .0      0
                                        'GigabitEthernet 1/4     ---    PoE Search         0 .0      0
                                        'GigabitEthernet 1/5     ---    PoE Search         0 .0      0
                                        'GigabitEthernet 1/6     ---    PoE Search         0 .0      0
                                        'GigabitEthernet 1/7     ---    PoE Search         0 .0      0
                                        'GigabitEthernet 1/8     ---    PoE Search         0 .0      0
                                        '----------------------  -----  -----------------  --------  ---------
                                        '
                                        'Current Power Consumption    9. 0[W] (2%)
                                        'PoE Voltage                 47. 5[V]
                                        'PoE Version==> 3.55(  0)
                                        '
                                        'WGS-5225-8UP2SV#
                                        '
                                        'Notes 1. all Planet Wisp switches denote port numbers as 1/whatever
                                        '         with 1/ being common across all models e.g. "1/3" means port 3
                                        '      2. PD Class 6 = 802.3bt device (a.k.a. PoE++ e.g. Ubiquiti port 1)
                                        '      3. PD CLass 4 = 802.3at device (a.k.a. POE+ e.g. Hikvision camera)
                                        '      4. Normally only a Ubiquiti port 1 would be connected to any
                                        '         WGS-5225-8UP2SV port
                                        '      5. PoE ON = power on, automatically negotiated as per PD class
                                        '      6. PoE disabled = power off
                                        '      7. PoE Search = no device attached

                                        wrk_port_lines = Regex.Split(Gen_crlf, wrk_result.Output)

                                        For Each wrk_port_line As String In wrk_port_lines

                                            'If a port line...
                                            If wrk_port_line.StartsWith("GigabitEthernet 1/") Then
                             
                                                'Strip out "GigabitEthernet 1/"
                                                wrk_port_line = wrk_port_line.Replace("GigabitEthernet 1/", "")
                             
                                                'Extract PoE Port number
                                                wrk_port_number = wrk_port_line.SubString2(0, 1)

                                                'Extract PoE Port current state
                                                If wrk_port_line.Contains("PoE ON") Then
                                                    wrk_port_poe.Put(wrk_port_number, "48V/H")
                                                Else
                                                    wrk_port_poe.Put(wrk_port_number, "Off")
                                                End If

                                            End If

                                        Next

                                    End If

                                End If
                 
                            End If

                        End If

                        'If failed (timeout?) somewhere above...
                        If wrk_result.Status = "Failure" Then
         
                            'Flag camera switch error
                            Gen_cameras_current_poe_state.Put(wrk_ptr, "???")

                        End If

                    'Otherwise, must be Ubiquiti switch...
                    Else
'Log("Ubiquiti show config")
                        'Send Ubiquiti command to show PoE status of all ports
                        wrk_commands_sent = "ubntbox swctrl poe show" & CRLF & "exit"
                        Obj_ssh.Write(wrk_commands_sent)

                        'Read until we see marker or failure (timeout?)
                        wrk_result = SSH_Read_Until(Obj_ssh, "# exit")
                 
'                        Log("ssh_Status ========================================")
'                        Log(wrk_result.Status)
'                        Log("ssh_Shell_commands_executed ========================================")
'                        Log(wrk_commands_sent)
'                        Log("ssh_Output.length ========================================")
'                        Log(wrk_result.Output.length)
'                        Log("ssh_Output ========================================")
'                        Log(wrk_result.Output)
'                        Log("========================================")
'                                    
'                        Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " =============================== show config ================================")
'                        Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " ssh Success: " & wrk_result.Status)
'                        Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " ssh Shell commands executed: " & wrk_commands_sent)
'                        Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " ssh Output Length: " & wrk_result.Output.length)
'                        Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " ssh Output: " & wrk_result.Output)
'                        Logger("Run " & DateTime.Date(DateTime.Now), DateTime.Time(DateTime.Now) & " ============================================================================")

                        'If got marker...
                        If wrk_result.Status = "Marker" Then

                            'If got to here wrk_result.Output should look like this:
                            '
                            '~l04262279:
                            '~l04262279:
                            'BusyBox v1.25.1 () built-in shell (ash)
                            '~l04262279:
                            '~l04262279:
                            '  ___ ___      .__________.__
                            ' |   |   |____ |__\_  ____/__|
                            ' |   |   /    \|  ||  __) |  |   (c) 2010-2023
                            ' |   |  |   |  \  ||  \   |  |   Ubiquiti Inc.
                            ' |______|___|  /__||__/   |__|
                            '            |_/                  https://www.ui.com
                            '~l04262279:
                            '      Welcome to UniFi USW-Flex!
                            '~l04262279:
                            '********************************* NOTICE **********************************
                            '* By logging in to, accessing, or using any Ubiquiti product, you are     *
                            '* signifying that you have read our Terms of Service (ToS) and End User   *
                            '* License Agreement (EULA), understand their terms, and agree to be       *
                            '* fully bound to them. The use of SSH (Secure Shell) can potentially      *
                            '* harm Ubiquiti devices and result in lost access to them and their data. *
                            '* By proceeding, you acknowledge that the use of SSH to modify device(s)  *
                            '* outside of their normal operational scope, or in any manner             *
                            '* inconsistent with the ToS or EULA, will permanently and irrevocably     *
                            '* void any applicable warranty.                                           *
                            '***************************************************************************
                            '~l04262279:
                            'USW-Flex-US.6.5.32# Total Power Limit(mW): 46000
                            '~l04262279:
                            'Port  OpMode      HpMode    PwrLimit   Class   PoEPwr  PwrGood  Power(W)  Voltage(V)  Current(mA)
                            '                              (mW)
                            '----  ------  ------------  --------  -------  ------  -------  --------  ----------  -----------
                            '   2    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                            '   3    Auto        Dot3at     31507  Class 4      On     Good      2.01       48.60         0.00
                            '   4    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                            '   5    Auto        Dot3at     31507  Class 4      On     Good      2.15       48.81        44.00
                            'USW-Flex-US.6.5.32# exit
                         
                            'Extract block of configuration details
                            wrk_block_start = wrk_result.Output.IndexOf("----  ------  ------------")
                            wrk_block_start = wrk_result.Output.IndexOf2("   2", wrk_block_start)
                            wrk_block_end = wrk_result.Output.IndexOf("# exit")
                            wrk_block_end = wrk_result.Output.LastIndexOf2(Gen_crlf, wrk_block_end)
                            wrk_block_str = wrk_result.Output.SubString2(wrk_block_start, wrk_block_end + 2)

                            'wrk_block_str should look like this:
                            '
                            '   2    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                            '   3    Auto        Dot3at     31507  Class 4      On     Good      2.01       48.60         0.00
                            '   4    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                            '   5    Auto        Dot3at     31507  Class 4      On     Good      2.15       48.81        44.00

                            wrk_port_lines = Regex.Split(Gen_crlf, wrk_block_str)

                            For Each wrk_port_line As String In wrk_port_lines

                                'wrk_port_line should look like this:
                                '
                                '   2    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                 
                                'Extract PoE Port number
                                wrk_port_number = wrk_port_line.SubString2(0, 4)
                     
                                'Extract PoE Port current state
                                If wrk_port_line.IndexOf(" Off ") = -1 Then
                                    wrk_port_poe.Put(wrk_port_number, "48V")
                                Else
                                    wrk_port_poe.Put(wrk_port_number, "Off")
                                End If

                            Next

                        'Otherwise, must have failed (timeout?)...
                        Else
                                                     
                            'Flag camera switch error
                            Gen_cameras_current_poe_state.Put(wrk_ptr, "???")
                     
                        End If

                    End If
                         
                    Obj_ssh.Disconnect
Note the elimination of the need for defining properties like:

Obj_ssh.SetPTY(True)
Obj_ssh.SetInitialNewline(False)

Also the optional error handling:

' 'Just do a normal LastException log
' Log(LastException)
'
' 'Alternatively decompose LastException
' Private wrk_exception As B4JSSH_SshException = LastException
' 'CONNECT / PROTOCOL / TIMEOUT / REMOTE_CLOSED / INTERNAL
' Log("SSH error kind: " & wrk_exception.Kind)
' Log("SSH error message: " & wrk_exception.Message)

DOCUMENTATION

ADDITIONAL NOTES
-
 

Attachments

  • B4JSSH.xml
    7.7 KB · Views: 50
  • B4JSSH.jar
    7.6 KB · Views: 51
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
Version 3.1 (Fully self contained version - no external jsch-0.1.55.jar required) - PREAMBLE/CODE/INSTALLATION/EXAMPLE/DOCUMENTATION/ADDITIONAL NOTES

PREAMBLE

As a (hopefully final) version I asked Copilot how I could embed the jsch-0.1.55.jar inside B4JSSH.jar.

I got nowhere fast so in frustration I uploaded the B4JSSH.java to Claude and asked it:

It responded with (I have edited it a little):

CODE
B4JSSH.java is unchanged except I bumped the version to 3.1

INSTALLATION
Load the attached files into your B4J Additional Libraries folder.

You don't have to bother with jsch-0.1.55.jar. If jsch-0.1.55.jar is in your B4j Additional Libraries folder then delete it.

In your B4J project select the B4JSSH library (make sure it is 3.1)

EXAMPLE

As per last post.

DOCUMENTATION
As per last post.

ADDITIONAL NOTES
-
 

Attachments

  • B4JSSH.jar
    286.4 KB · Views: 50
  • B4JSSH.xml
    7.7 KB · Views: 50
Last edited:

pixet

Active Member
Licensed User
Longtime User
Hi,
I'm testing version 3.1 with JDK 19.0.2 in Windows 11, B4J v.10.50.
Commands sent by the ssh.Write(string) command seem to be executed correctly.

They use try/catch and formatting error responses with
Formatting error strings responses with:
Dim ex As B4JSSH_SshException = LastException
Log("SSH Error Kind = " & ex.Kind)
Log("Message = " & ex.Message)

from an error very different from what I expect (host not found - or - timeout: socket is not established) goes like this

Debug log errors:
main$ResumableSub_btnStart_Click.resume (java line: 263)
java.lang.ClassCastException: class com.jcraft.jsch.JSchException cannot be cast to class b4j.ssh.SshException (com.jcraft.jsch.JSchException and b4j.ssh.SshException are in unnamed module of loader 'app')
at b4j.example.main$ResumableSub_btnStart_Click.resume(main.java:263)
at b4j.example.main._btnstart_click(main.java:172)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:578)
at anywheresoftware.b4a.BA.raiseEvent2(BA.java:117)
at anywheresoftware.b4a.BA$1.run(BA.java:242)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:457)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:456)
at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)
at java.base/java.lang.Thread.run(Thread.java:1589)


If I use "ReadUntil" an error is generated:

ReadUntil error:
Error: (ClassCastException) java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String; ([Ljava.lang.Object; and [Ljava.lang.String; are in module java.base of loader 'bootstrap')

I added the example I'm trying
 

Attachments

  • test_02_fx.zip
    3.1 KB · Views: 41

JackKirk

Well-Known Member
Licensed User
Longtime User
pixet, sorry I don't have a raspberry to test this on, in the hope I may be helping here is a snippet of code from my B4J project that uses B4JSSH to manage Netonix, Planet and Ubiquiti WISP switches in a WIndows 10 environment:
B4X:
                    Try
                       
                        'Connect
                        Obj_ssh.Connect(wrk_switch_ip, wrk_switch_ssh_port, wrk_camera.switch_user_name, wrk_camera.switch_password, Gen_switch_contact_timeout)

                        'Open interactive shell
                        Obj_ssh.OpenShell

                    Catch
                       
'                        'Just do a normal LastException log
'                        Log(LastException)
'                      
'                        'Alternatively decompose LastException
'                        Private wrk_exception As B4JSSH_SshException = LastException
'                        'CONNECT / PROTOCOL / TIMEOUT / REMOTE_CLOSED / INTERNAL                      
'                        Log("SSH error kind: " & wrk_exception.Kind)
'                        Log("SSH error message: " & wrk_exception.Message)
                       
                        'Close failed session
                        Obj_ssh.Disconnect
                           
                        'Stop processing current camera and move to next one
                        Continue
                       
                    End Try
                       
                    wrk_port_poe.Clear

                    'If Netonix switch...
                    If wrk_camera.switch_type = "Netonix" Then

                        'Ask switch for current configuration details
                        wrk_commands_sent = "terminal length 0" & CRLF & "show config" & CRLF & "exit"
                        Obj_ssh.Write(wrk_commands_sent)

                        'Read until we see Netonix prompt or failure (timeout?)
                        wrk_result = SSH_Read_Until(Obj_ssh, wrk_switch_name & "# exit")

                        'If got Netonix prompt...
                        If wrk_result.Status = "Marker" Then
                       
                            'Extract JSON which is configuration details
                            wrk_json_start = wrk_result.Output.IndexOf("{")
                            wrk_json_end = wrk_result.Output.IndexOf(wrk_switch_name & "# exit")
                            wrk_json_str = wrk_result.Output.SubString2(wrk_json_start, wrk_json_end)

                            'Extract PoE Ports configuration details
                            wrk_ports_start = wrk_json_str.IndexOf("""Ports"": [")
                            wrk_ports_end = wrk_json_str.IndexOf2("],", wrk_ports_start)
                            wrk_ports_str = wrk_json_str.SubString2(wrk_ports_start, wrk_ports_end)
                            wrk_port_number_end = 0

                            'Loop forever...
                            Do While True
               
                                'Locate start of next PoE Port number field
                                wrk_port_number_start = wrk_ports_str.IndexOf2("""Number"":", wrk_port_number_end) + 10
               
                                'Quit loop if no more
                                If wrk_port_number_start = 9 Then Exit
               
                                'Locate end of PoE Port number field
                                wrk_port_number_end = wrk_ports_str.IndexOf2(",", wrk_port_number_start)
               
                                'Extract PoE Port number
                                wrk_port_number = wrk_ports_str.SubString2(wrk_port_number_start, wrk_port_number_end)
               
                                'Locate start of next PoE Port state field
                                wrk_port_poe_start = wrk_ports_str.IndexOf2("""PoE"":", wrk_port_number_end) + 8
               
                                'Locate end of PoE Port state field
                                wrk_port_poe_end = wrk_ports_str.IndexOf2(""",", wrk_port_poe_start)
               
                                'Extract PoE Port current state
                                wrk_port_poe.Put(wrk_port_number, wrk_ports_str.SubString2(wrk_port_poe_start, wrk_port_poe_end))
                   
                            Loop
                   
                        'Otherwise, must have failed (timeout?)...
                        Else
                   
                            'Flag camera switch error

                        End If

                    'Otherwise, if Planet switch...
                    Else If wrk_camera.switch_type = "Planet" Then

                        'Read until we see Planet Username: prompt or failure (timeout?)
                        wrk_result = SSH_Read_Until(Obj_ssh, "Username:")
                       
                        'If got Planet Username: prompt...
                        If wrk_result.Status = "Marker" Then
                       
                            'Send Planet switch username
                            wrk_commands_sent = wrk_camera.switch_user_name
                            Obj_ssh.Write(wrk_commands_sent)

                            'Read until we see Planet Password: prompt or failure (timeout?)
                            wrk_result = SSH_Read_Until(Obj_ssh, "Password:")
                               
                            'If got Planet Password: prompt...
                            If wrk_result.Status = "Marker" Then
                                   
                                'Send Planet switch password
                                wrk_commands_sent = wrk_camera.switch_password
                                Obj_ssh.Write(wrk_commands_sent)
                                   
                                'Read until we see Planet # prompt or failure (timeout?)
                                wrk_result = SSH_Read_Until(Obj_ssh, "#")
                                   
                                'If got Planet # prompt...
                                If wrk_result.Status = "Marker" Then
                                   
                                    'Send Planet command to show PoE status of all PoE ports
                                    wrk_commands_sent = "show poe"
                                    Obj_ssh.Write(wrk_commands_sent)

                                    'Read until we see Planet prompt or failure (timeout?)
                                    wrk_result = SSH_Read_Until(Obj_ssh, "#")

                                    'If got Planet # prompt...
                                    If wrk_result.Status = "Marker" Then

                                        wrk_port_lines = Regex.Split(Gen_crlf, wrk_result.Output)

                                        For Each wrk_port_line As String In wrk_port_lines

                                            'If a port line...
                                            If wrk_port_line.StartsWith("GigabitEthernet 1/") Then
                                   
                                                'Strip out "GigabitEthernet 1/"
                                                wrk_port_line = wrk_port_line.Replace("GigabitEthernet 1/", "")
                                   
                                                'Extract PoE Port number
                                                wrk_port_number = wrk_port_line.SubString2(0, 1)

                                                'Extract PoE Port current state
                                                If wrk_port_line.Contains("PoE ON") Then
                                                    wrk_port_poe.Put(wrk_port_number, "48V/H")
                                                Else
                                                    wrk_port_poe.Put(wrk_port_number, "Off")
                                                End If

                                            End If

                                        Next

                                    End If

                                End If
                       
                            End If

                        End If

                        'If failed (timeout?) somewhere above...
                        If wrk_result.Status = "Failure" Then
               
                            'Flag camera switch error

                        End If

                    'Otherwise, must be Ubiquiti switch...
                    Else

                        'Send Ubiquiti command to show PoE status of all ports
                        wrk_commands_sent = "ubntbox swctrl poe show" & CRLF & "exit"
                        Obj_ssh.Write(wrk_commands_sent)

                        'Read until we see marker or failure (timeout?)
                        wrk_result = SSH_Read_Until(Obj_ssh, "# exit")
                       
                        'If got marker...
                        If wrk_result.Status = "Marker" Then

                            'Extract block of configuration details
                            wrk_block_start = wrk_result.Output.IndexOf("----  ------  ------------")
                            wrk_block_start = wrk_result.Output.IndexOf2("   2", wrk_block_start)
                            wrk_block_end = wrk_result.Output.IndexOf("# exit")
                            wrk_block_end = wrk_result.Output.LastIndexOf2(Gen_crlf, wrk_block_end)
                            wrk_block_str = wrk_result.Output.SubString2(wrk_block_start, wrk_block_end + 2)

                            wrk_port_lines = Regex.Split(Gen_crlf, wrk_block_str)

                            For Each wrk_port_line As String In wrk_port_lines
                       
                                'Extract PoE Port number
                                wrk_port_number = wrk_port_line.SubString2(0, 4)
                           
                                'Extract PoE Port current state
                                If wrk_port_line.IndexOf(" Off ") = -1 Then
                                    wrk_port_poe.Put(wrk_port_number, "48V")
                                Else
                                    wrk_port_poe.Put(wrk_port_number, "Off")
                                End If

                            Next

                        'Otherwise, must have failed (timeout?)...
                        Else
                                                           
                            'Flag camera switch error
                           
                        End If

                    End If
                               
                    Obj_ssh.Disconnect

                End If
I have chopped a whole lot of code and comments out of the above.

and SSH_Read_Until is:
B4X:
'************************************************************************************
'
'This procedure reads data from SSH_object until a marker is detected or failure
'(timeout?)
'
'Input parameters are:
'
'       SSH_object = SSH object
'       Marker = marker to be checked for
'
'Returns:
'
'       A result as a SSH_Read_Result_Type
'
'Notes on this procedure:
'
'       o None
'
'************************************************************************************
Private Sub SSH_Read_Until(SSH_object As B4JSSH, Marker As String) As SSH_Read_Result_Type
   
    Private wrk_result As SSH_Read_Result_Type

    Try
        wrk_result.Output = SSH_object.ReadUntil(Array As String(Marker), Gen_switch_contact_timeout)
        wrk_result.Status = "Marker"
    Catch
        wrk_result.Output = LastException.Message
        wrk_result.Status = "Failure"
    End Try

    Return wrk_result
   
End Sub

and you may need to change

out = ssh.ReadUntil(Array("[sudo]", "password"), 5000)

to

out = ssh.ReadUntil(Array As String("[sudo]", "password"), 5000)

Let me know how you get on...
 

JackKirk

Well-Known Member
Licensed User
Longtime User
pixet, I fed your error log to Copilot who had an "Oh yeah..." moment and came up with a small fix - see Version 3,2 below,,,
 
Cookies are required to use this site. You must accept them to continue using the site. Learn more…