Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.acton.lock
.build
out
zig-cache
zig-out
83 changes: 72 additions & 11 deletions src/ssh.act
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,32 +1,45 @@
import net
import testing


def version() -> str:
"""Get the libssh version"""
"""Get the acton-ssh version"""
NotImplemented

def _test_version():
testing.assertEqual("0.11.0", version())

actor Client(cap: net.TCPConnectCap,
host: str,
username: str,
on_connect: action(Client) -> None,
on_close: action(Client, str) -> None,
key: ?str=None,
password: ?str=None,
password: str,
port: u16=22,
subsystem: str
):
"""SSH Client"""

proc def _pin_affinity() -> None:
NotImplemented
_pin_affinity()

# haha, this is really a pointer :P
var _ssh_session: u64 = 0

def get_ssh_session():
return _ssh_session

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
action def get_affinity() -> u64:
NotImplemented

then need to implement the necessary C call to return the actual affinity of the Client actor. We need this as action so the Channel actor can query the Client about affinity, so the Channel actor in turn can make sure it gets pinned to the same worker thread as the Client.

def get_password() -> str:
return password

def get_subsystem() -> str:
return subsystem

proc def _init() -> None:
"""Initialize the SSH client"""
NotImplemented
_init()
print("SSH Client connected")

# print("SSH Client connected")

# action def close(on_close: action(TLSConnection) -> None) -> None:
# """Close the connection"""
Expand All @@ -37,33 +50,81 @@ actor Client(cap: net.TCPConnectCap,
#
# def _connect(c):
# NotImplemented
#

proc def disconnect() -> None:
"""Disconnect the SSH client"""
NotImplemented


# TODO: implement support for channels
# AFAIK, all things over ssh are done via channels, so need some channel
# primitive, maybe an actor per channel that then multiplexes into the Client
# session? Prolly need some higher level wrappers for common things like
# starting a shell or running a single command. SFTP / SCP would be nice too,
# but for sometime in the future. Custom subsystems need to be supported too.
actor Channel(client: Client):
"""SSH Channel"""

var _ssh_channel: u64 = 0
var _ssh_session: u64 = client.get_ssh_session()
var _password: str = client.get_password()
var _subsystem: str = client.get_subsystem()
var payload = None

proc def _pin_affinity() -> None:
NotImplemented
_pin_affinity()

proc def _init() -> None:
"""Initialize the SSH Channel"""
NotImplemented
_init()
Comment on lines +78 to +81
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
proc def _init() -> None:
"""Initialize the SSH Channel"""
NotImplemented
_init()
proc def _ssh_init() -> None:
"""Initialize the SSH Channel"""
NotImplemented
def _set_affinity(affinity: u64) -> None:
NotImplemented
def _init():
aff = client.get_affinity()
set_affinity(aff)
after 0: _ssh_init()
_init()

the channel actor needs to be running on the same worker thread as the Client, since it accesses the same socket and accessing the socket in our libuv IO loop means we need to be running on the same worker therad. libuv is not thread safe, so accessing the IO loop of another worker thread will cause crashes. It might be working for you while developing today, but it's because there is so little going on. If we write a test with 1000 concurrently running SSH clients, it would mostly likely segfault.

Actor continuations are run by worker threads. Per default they go to the global shared readyQ and then a "random" worker thread picks it up from there. The actor initialization (running the code body of the actor) always runs in a random worker, so the first thing it should do is set affinity, then any subsequent continuations will run in the correct worker thread. Thus, we must NOT run the ssh init stuff too early, that's why I'm suggesting here to first set affinity then do after 0: ... which schedules that method to run, so the correct worker thread can pick it up.

There are some affinity related functions in rts.h or around there. See the other IO actors that interact with it. We can read out our affinity by reading the $Actor->affinity attribute (or whatever it is called).

Let me know if you have any questions and we can walk through it! :)


# print("SSH Channel created")

def setPayload(p: str):
payload = p

def getPayload():
return payload

def sendNCPayload() -> str:
"""Send payload"""
NotImplemented


actor main(env):
def on_connect(client: Client):
print("Connected")
# print("Client connected")
return

def on_close(client: Client, error: str):
print("Error", error)
# print("Connection closed", error)
return

print(version())
c = Client(
net.TCPConnectCap(net.TCPCap(net.NetCap(env.cap))),
"localhost",
"foo",
on_connect,
on_close,
password="bar",
port=2223,
port=830,
subsystem="netconf",
)

cc = Channel(c)

# get netconf server's capabilities
cc.setPayload('<?xml version="1.0"?><rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"message-id="101"><get-config><source><running/></source></get-config></rpc>]]>]]>')
print("\n\npayload 1:\n", cc.getPayload(), "\n\npayload 1 end")
print("\nNC response 1:\n", cc.sendNCPayload(), "\n\nNC response 1 end \n\n")

# TODO do this in disconnect(): gracefully close a channel
cc.setPayload('<rpc message-id="101" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"><close-session/></rpc>]]>]]>')
print("\n\npayload 2:\n", cc.getPayload(), "\n\npayload 2 end")
print("\nNC response 2:\n", cc.sendNCPayload(), "\n\nNC response 2 end \n\n")

c.disconnect()

env.exit(0)
Loading