summaryrefslogtreecommitdiff
path: root/src/tmate/session.vala
blob: 356aa6686c680349442be26403b3fc922aca843d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
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<string>();
				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<string>();
				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;
				}
			}
	}
}