namespace Tmate { public interface SessionInterface : Object { public abstract signal void started (string socket); public abstract signal void stopped (); public abstract signal void address_changed (SessionType session, string address); public abstract signal void error (string summery, string message = ""); public abstract signal void joined (string mate, int mates); public abstract signal void left (string mate, int mates); public abstract bool start(Config? config = null); public abstract bool stop(); } [SingleInstance] public class Session: Object, SessionInterface { private IOChannel? stdout = null; private Pid? pid = null; private uint error_count = 0; public bool terminate = true; public SessionAddress address { get; construct; } public string? socket { get; private set;} public Config? config { get; private set;} construct { address = new SessionAddress(); reset(); address.clear(); error.connect_after(() => { if(error_count++ < 5) return; stop(); }); } private void address_set(SessionType session, string? address) { if (SessionType.SSH in session || SessionType.HTTP in session) { debug("CALL: %s\t%s", session.to_string(), address); if(address != null || address != "") this.address.add(session, address); if(this.address[session] != null) address_changed (session, this.address[session]); } } private void reset() { stdout = null; pid = null; socket = null; error_count = 0; address.clear(); delete_config(); } internal void delete_config() { if(null == config) return; config.delete(); config = null; } private bool send(string[] args) { if(pid == 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; } } private void restart() { if(terminate) { stop(); return; } started(socket); address_set(SSH, null); address_set(HTTP, null); address_set(SSH | READONLY, null); address_set(HTTP | READONLY, null); } public bool stop() { debug("stopping!"); var ret = send({"kill-server"}); if (! send({"has"})) { debug("stopped!"); Idle.add(() => { stopped(); return false; }); } return ret; } public bool start(Config? config = null) { if(null != pid) { started(socket); return true; } int out_fd; var args = new GenericArray(); args.data = {"/usr/bin/tmate", "-F"}; if(null != config) { this.config = config; args.add("-f"); args.add(config.get_path()); } 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; channel.read_line (out line, null, null); Stdout.Token token = (new Stdout()).parse(line); debug(token.to_string()); switch(token.class) { case SOCKET: socket = token.list[0]; return true; case INIT: started (socket); delete_config(); return true; case ADDRESS: address_set( SessionType.from_string( token.list[0], token.list[1] ), token.list[2]); return true; case RESTART: restart(); return true; case TERM: info (_("Session terminated.")); stopped(); return false; case ERROR: this.error(token.list[0], token.list[1]); return true; case COMMENT: return true; // not needed. case MATES: return true;// TODO: handle mates join/leave case UNKNOWN: debug("%s", token.list[0]); return true; } } 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); reset(); stopped(); }); return true; } catch (SpawnError e) { warning("Error: %s", e.message); this.error(e.message); reset(); stopped(); return false; } } } }