aboutsummaryrefslogtreecommitdiff
path: root/norns_shell.c
blob: 425e6051a02e3188436af53745249f2931a6a122 (plain)
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
253
254
255
256
257
258
259
260
261
#include <time.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>

#include <nanomsg/nn.h>
#include <nanomsg/bus.h>
#include <nanomsg/ws.h>

#define MAX_LINE 1024

#define S(n) (1000000000 * ((long) n))
#define MS(n) (1000000 * ((long) n))

#define SENTINEL_TIMEOUT (MS(1500))
#define SENTINEL_DELAY (MS(200))
#define WAIT (MS(200))
#define TOTAL_WAIT (S(2))

int time_diff_gt(struct timespec *before, struct timespec *after, long ns)
{
	/* be careful not to overflow the long... */
	long ss = ns / S(1) + 1;
	long ss_diff = after->tv_sec - before->tv_sec;
	if (ss_diff > ss)
		return 1;
	long ns_diff = ss_diff * S(1) + (after->tv_nsec - before->tv_nsec);
	return ns_diff > ns;
}

int killed = 0;
void handle_signal(int sig_num)
{
	killed = 1;
}


int main (int argc, char **argv)
{
	char *endpoint = "ws://norns.local:5555";
	int exit = 0;
	int s = 0;

	signal(SIGINT, handle_signal);
	signal(SIGTERM, handle_signal);

	/* Configure the websocket endpoint. */
	if (argc > 2 ||
	    (argc == 2 &&
	     (!strcmp(argv[1], "--help")
	      || !strcmp(argv[1], "-h")))) {
		fprintf(stderr,
			"Get a lua or supercollider shell on a norns.\n"
			"Usage: %s [ws://norns.local:5555]\n",
			argv[0]);
		goto error;
	}
	if (argc == 2) endpoint = argv[1];

	/* connect to the norns; use text, like it expects. */
	if ((s = nn_socket(AF_SP, NN_BUS)) < 0) {
		fprintf(stderr, "Can't create socket: %m\n");
		goto error;
	}
	if (nn_connect(s, endpoint) < 0) {
		fprintf(stderr, "Can't connect to %s: %m\n", endpoint);
		goto error;
	}
	if (nn_setsockopt(s,
			  NN_WS,
			  NN_WS_MSG_TYPE,
			  &(int) {NN_WS_MSG_TYPE_TEXT},
			  sizeof(int)) < 0) {
		fprintf(stderr, "Couldn't switch to text mode: %m\n");
		goto error;
	}

	/* since nanomsg has its own poll framework, we'll read to this
	   buffer in a nonblocking way until we get a newline. */
	size_t line_n = 0;
	char line[MAX_LINE + 1] = {0};
	
	int fl = 0;
	if ((fl = fcntl(STDIN_FILENO, F_GETFL)) < 0) {
		fprintf(stderr, "Couldn't read stdin fd flags: %m\n");
		goto error;
	}
	if (fcntl(STDIN_FILENO, F_SETFL, fl | O_NONBLOCK)) {
		fprintf(stderr, "Couldn't set stdin non-blocking: %m\n");
		goto error;
	}

	/* NN_BUS doesn't guarantee delivery, unfortunately.
	   in practice on a LAN this means that the first couple of
	   messages are dropped while the connection is being set up.
	   this seems strange to me (it seems like you'd have to go
	   out of your way to drop messages in a websocket handler?)
	   and I'm not sure this is the right fit for norns, but that's
	   how it is.

	   we partially solve this issue by sending an empty comment
	   -- as a sentinel -- repeatedly until we get the corresponding
	   <ok>.
	*/
	/* Meanwhile, there are several different timeouts being checked
	   here:

	   1) If nn_poll times out, it means the fd was neither writable
	      nor readable during the specified amount of time. This means
	      something's seriously wrong.
	   2) If we don't receive a response to the sentinel within TIMEOUT,
	      this means the endpoint probably isn't responding, and there
	      may not be a server at the endpoint at all. We'll exit in this
	      case instead of leaving the user hanging forever.
	   3) If we send the final message (i.e., if we got EOF), we'll exit
	      if we don't receive anything within WAIT of the last time we
	      received anything. This way we exit quickly if there's no more
	      responses, but if there's a flurry of quick responses we get them all.
	   4) However, if the lua engine just keeps printing, then we don't
	      want to leave the user hanging forever. So if we sent the final
	      message (i.e., if we got EOF) more than TOTAL_WAIT ago, we exit.
	*/
	int received = 0;
	int sent_last = 0;
	struct timespec input_ended = {0};
	struct timespec last_received = {0};
	struct timespec began = {0};
	if (clock_gettime(CLOCK_MONOTONIC, &began)) {
		fprintf(stderr, "Can't use the clock? %m\n");
		goto error;
	}
	struct timespec sent_sentinel = {0};

	/* we'll also wait for one message after EOF... */

	struct nn_pollfd poll_s = { .fd = s, .events = NN_POLLIN | NN_POLLOUT };
	while (!killed) {
		struct timespec now = {0};
		switch (nn_poll(&poll_s, 1, 1000)) {
		case -1:
			fprintf(stderr, "Error polling: %m\n");
			goto error;
		case 0:
			fprintf(stderr, "# poll timeout. something is probably wrong.\n");
			goto error;
		case 1:
			if (clock_gettime(CLOCK_MONOTONIC, &now)) {
				fprintf(stderr, "Can't use the clock? %m\n");
				goto error;
			}
			/* If we've sent the last message and we're over the wait
			   since the last received message, exit. */
			if (sent_last && time_diff_gt(&last_received, &now, WAIT))
				goto finish;
			/* If we've sent the last message and we're over the wait
			   since we ended, exit. */
			if (sent_last && time_diff_gt(&input_ended, &now, TOTAL_WAIT))
				goto finish;
			/* If we can read, echo out the message (unless it's the
			   response to the sentinel, which we handle differently). */
			if (poll_s.revents & NN_POLLIN) {
				char *buf = NULL;
				size_t n = 0;
				if ((n = nn_recv(s, &buf, NN_MSG, 0)) < 0) {
					fprintf(stderr, "Couldn't receive: %m\n");
					goto error;
				}
				/* swallow the first <ok> */
				if (!received && !strncmp(buf, "<ok>\n\n", n)) {
					fprintf(stderr, "# it lives!\n");
				} else {
					/* strip final newline, if there are two... */
					if (n >= 2
					    && buf[n - 1] == '\n'
					    && buf[n - 2] == '\n')
						n -= 1;
					if (write(STDOUT_FILENO, buf, n) < n) {
						fprintf(stderr, "Couldn't write: %m\n");
						goto error;
					}
				}
				/* TODO: maybe if we get a stacktrace we should set the
				   exit code? */
				nn_freemsg(buf);
				if (clock_gettime(CLOCK_MONOTONIC, &last_received)) {
					fprintf(stderr, "Can't use the clock? %m\n");
					goto error;
				}
				received = 1;
			}
			/* If we can write, see if there's a full line. */
			if (poll_s.revents & NN_POLLOUT) {
				if (!received) {
					/* if we've been waiting on sentinel response and
					   haven't gotten anything, time out. */
					if (time_diff_gt(&began, &now, SENTINEL_TIMEOUT)) {
						fprintf(stderr,
							"# timed out.\n"
							"# are you sure there's something "
							"listening at %s?\n",
							endpoint);
						goto error;
					}
					/* only do this once every SENTINEL_DELAY... */
					if (!time_diff_gt(&sent_sentinel,
							  &now,
							  SENTINEL_DELAY))
						break;
					memcpy(&sent_sentinel, &now, sizeof(struct timespec));
					
					/* send the sentinel comment */
					if (nn_send(s, "--\n", 3, 0) != 3) {
						fprintf(stderr, "Couldn't send: %m\n");
						goto error;
					}
				} else {
					int n = 0;
					if ((n = read(STDIN_FILENO,
						      &line[line_n],
						      MAX_LINE - line_n)) < 0) {
						if (errno != EAGAIN) {
							fprintf(stderr, "Can't read: %m\n");
							goto error;
						}
					}
					if (n == 0) {
						sent_last = 1;
						if (clock_gettime(CLOCK_MONOTONIC, &input_ended)) {
							fprintf(stderr, "Can't use the clock? %m\n");
							goto error;
						}
					}
					/* treat EAGAIN as "read 0" */
					if (n < 1) n = 0;
					line_n += n;
					if (line_n >= 1 && line[line_n - 1] == '\n') {
						if ((nn_send(s, line, line_n, 0)) != line_n) {
							fprintf(stderr, "Can't send: %m\n");
							goto error;
						}
						line_n = 0;
					}
					if (line_n == MAX_LINE) {
						fprintf(stderr, "Line too long :(.\n");
						goto error;
					}
				}
			}
			break;
		}
	}
	goto finish;
error:
	exit = 1;
finish:
	if (s > 0) nn_close(s);
	return exit;
}