namespace Tmate { public interface SessionInterface : Object { public abstract signal void address (SessionType session, string address); public abstract string? @get(SessionType session); public abstract bool start(string? config = null); public abstract bool stop(); } [SingleInstance] public class Session: Object, SessionInterface { private string? _addr_ssh = null; private string? _addr_http = null; private string? _addr_ssh_ro = null; private string? _addr_http_ro = null; private IOChannel? stdout = null; private Pid? pid = null; private uint error_count = 0; // Regex private Regex r_connect = /^Connecting to .+\.\.\.$/; private Regex r_socket = /^To connect.+ tmate -S (.+) attach$/; private Regex r_http = /^web \w+[:] (.+)$/; private Regex r_ssh = /^ssh \w+[:] .+ (.+)+$/; private Regex r_http_ro = /^web \w+ read \w+[:] (.+)$/; private Regex r_ssh_ro = /^ssh \w+ read \w+[:] .+ (.+)+$/; private Regex r_joined = /^.+ joined \(([^()]+)\) -- (\d+) clients? .+$/; private Regex r_left = /^.+ left \(([^()]+)\) -- (\d+) clients? .+$/; private Regex r_closed = /^Session closed$/; private Regex r_note = /^(Note: |Reconnecting)/; private Regex r_restarted = /^Session shell restarted$/; private Regex r_netfail= /^[^ ]+ lookup failure\..+ \((.+)\)$/; public SessionType flags { get; private set;} public string? socket { get; private set;} public bool terminate = true; construct { reset(); } public virtual signal void started (string socket) { info (_("Session: unix://%s"), socket); } public virtual signal void network_error(string message) { if(error_count ++ > 5) stop(); } public virtual signal void stopped () { info (_("Session terminated.")); reset(); } private void address_set(SessionType session, string address) { info (_("%s address: %s"), session.to_string(), address); if (session in SessionType.SSH|SessionType.SSH_READONLY|SessionType.HTTP|SessionType.HTTP_READONLY) { switch(session) { case SSH: _addr_ssh = address; break; case HTTP: _addr_http = address; break; case HTTP_READONLY: _addr_http_ro = address; break; case SSH_READONLY : _addr_ssh_ro = address; break; default: warn_if_reached(); break; } flags |= session; this.address(session, address); } else if (session == DISCONNECTED) { _addr_http = _addr_ssh = _addr_http_ro = _addr_ssh_ro = null; flags = DISCONNECTED; this.address(DISCONNECTED, ""); } } public virtual signal void joined (string mate, int mates) { info(_("%s joined, connected: %d"), mate, mates); } public virtual signal void left (string mate, int mates) { info(_("%s left, connected: %d"), mate, mates); } private void reset() { _addr_ssh = null; _addr_http = null; _addr_ssh_ro = null; _addr_http_ro = null; stdout = null; pid = null; socket = null; flags = DISCONNECTED; error_count = 0; } private bool send(string[] args) { if(socket == null) return false; var command = new GenericArray(); command.data = { "/usr/bin/tmate", "-S", socket }; foreach(var arg in args) command.add(arg); try { return Process.spawn_sync(null, command.data, null, STDERR_TO_DEV_NULL|STDOUT_TO_DEV_NULL, null, null, null, null); } catch (SpawnError e) { return false; } } public new string? @get(SessionType session) { switch(session) { case SSH: return _addr_ssh; case HTTP: return _addr_http; case SSH_READONLY : return _addr_ssh_ro; case HTTP_READONLY: return _addr_http_ro; case DISCONNECTED : warning("No running session."); break; default: warning("Unknown Session Type %s", session.to_string()); break; } return null; } public bool stop() { return send({"kill-server"}); } private void restart() { if(terminate) { stop(); return; } started(socket); if((bool)_addr_ssh) address(SSH, _addr_ssh); if((bool)_addr_ssh_ro) address(SSH_READONLY, _addr_ssh_ro); if((bool)_addr_http) address(HTTP, _addr_http); if((bool)_addr_http_ro) address(HTTP_READONLY, _addr_http_ro); } public bool start(string? config = null) { if(! (pid == null)) { started(socket); return true; } int out_fd; var args = new GenericArray(); args.data = {"/usr/bin/tmate", "-F"}; if(! (config == null) && FileUtils.test(config, IS_REGULAR)) { args.add("-f"); args.add(config); } try { Process.spawn_async_with_pipes( null, args.data, null, STDERR_TO_DEV_NULL | DO_NOT_REAP_CHILD, null, out pid, null, out out_fd, null ); stdout = new IOChannel.unix_new(out_fd); stdout.add_watch(IOCondition.IN | IOCondition.HUP, (channel, condition) => { if (condition == IOCondition.HUP) return false; try { string line; MatchInfo info; channel.read_line (out line, null, null); if(r_socket.match(line, 0, out info)) socket = info.fetch(1); else if (r_connect.match(line) && socket != null) started(socket); else if(r_http.match(line, 0, out info)) address_set(HTTP, info.fetch(1)); else if(r_http_ro.match(line, 0, out info)) address_set(HTTP_READONLY, info.fetch(1)); else if(r_ssh.match(line, 0, out info)) address_set(SSH, info.fetch(1)); else if(r_ssh_ro.match(line, 0, out info)) address_set(SSH_READONLY, info.fetch(1)); else if(r_restarted.match(line)) restart(); else if(r_closed.match(line)) stopped(); else if(r_netfail.match(line, 0, out info)) network_error(info.fetch(1)); else if(r_note.match(line)) unlikely(false); // Ignore line else debug("Unprocessed line: \"%s\"", line); } catch (IOChannelError e) { print ("IOChannelError: %s\n", e.message); return false; } catch (ConvertError e) { print ("ConvertError: %s\n", e.message); return false; } return true; }); ChildWatch.add(pid, (pid, status) => { Process.close_pid(pid); if(flags != DISCONNECTED) stopped(); }); return true; } catch (SpawnError e) { warning("Error: %s", e.message); stopped(); return false; } } } }