1 /*
2  * arcan-clipboard (aclip)
3  * Copyright 2017-2018, Björn Ståhl
4  * License: 3-Clause BSD, see COPYING file in arcan source repository.
5  * Description: Clipboard Integration tool
6  */
7 #include <arcan_shmif.h>
8 #include <getopt.h>
9 #include <signal.h>
10 #include "utf8.c"
11 
12 static char* separator = "";
13 
paste(struct arcan_shmif_cont * out,char * msg,bool cont)14 static void paste(struct arcan_shmif_cont* out, char* msg, bool cont)
15 {
16 	arcan_event msgev = {
17 		.ext.kind = ARCAN_EVENT(MESSAGE)
18 	};
19 
20 	if (!out || !out->vidp || !msg)
21 		return;
22 
23 /* split into message size blocks, align backwards so full UTF8
24  * seqs are passed, else server may dismiss or kill us */
25 	size_t len = strlen(msg);
26 
27 	uint32_t state = 0, codepoint = 0;
28 	char* outs = msg;
29 	size_t maxlen = sizeof(msgev.ext.message.data) - 1;
30 
31 	while (len > maxlen){
32 		size_t i, lastok = 0;
33 		state = 0;
34 		for (i = 0; i <= maxlen - 1; i++){
35 			if (UTF8_ACCEPT == utf8_decode(&state, &codepoint, (uint8_t)(msg[i])))
36 				lastok = i;
37 
38 			if (i != lastok){
39 				if (0 == i)
40 					return;
41 			}
42 		}
43 
44 		memcpy(msgev.ext.message.data, outs, lastok);
45 		msgev.ext.message.data[lastok] = '\0';
46 		len -= lastok;
47 		outs += lastok;
48 		if (len)
49 			msgev.ext.message.multipart = 1;
50 		else
51 			msgev.ext.message.multipart = cont;
52 
53 		arcan_shmif_enqueue(out, &msgev);
54 	}
55 
56 /* flush remaining */
57 	if (len){
58 		snprintf((char*)msgev.ext.message.data, maxlen, "%s", outs);
59 		msgev.ext.message.multipart = cont;
60 		arcan_shmif_enqueue(out, &msgev);
61 	}
62 }
63 
write_ev(arcan_event * ev,FILE * outf)64 static bool write_ev(arcan_event* ev, FILE* outf)
65 {
66 /* if it's a continuation, we can't write the separator yet, just find the
67  * end of the buffer or where the string was truncated and terminated */
68 	if (ev->tgt.ioevs[0].iv != 0){
69 		size_t i = 0;
70 		for (; i < sizeof(ev->tgt.message); i++)
71 			if (ev->tgt.message[i] == '\0')
72 				break;
73 		if (i == 0)
74 			return false;
75 		if (i == sizeof(ev->tgt.message))
76 			i++;
77 		fwrite(ev->tgt.message, i, 1, outf);
78 		return false;
79 	}
80 	else{
81 		fprintf(outf, "%s%s", ev->tgt.message, separator);
82 		fflush(outf);
83 		return true;
84 	}
85 }
86 
dispatch_event_outf(arcan_event * ev,FILE * outf)87 static int dispatch_event_outf(arcan_event* ev, FILE* outf)
88 {
89 	switch (ev->tgt.kind){
90 	case TARGET_COMMAND_MESSAGE:
91 		if (write_ev(ev, outf))
92 			return 1;
93 	break;
94 	case TARGET_COMMAND_NEWSEGMENT:
95 /* if we explicitly receive a new paste board due to some incoming stream */
96 	break;
97 	case TARGET_COMMAND_BCHUNK_IN:
98 	break;
99 	case TARGET_COMMAND_BCHUNK_OUT:
100 /* incoming binary blob, fd in ioevs[0].iv */
101 	break;
102 	case TARGET_COMMAND_STEPFRAME:
103 /* need to distinguish if we are an output segment or not, in the latter
104  * case, we just received a raw video buffer to work with */
105 	break;
106 	case TARGET_COMMAND_EXIT:
107 		return -1;
108 	break;
109 	default:
110 	break;
111 	}
112 	return 0;
113 }
114 
dispatch_event_outcmd(struct arcan_event * ev,const char * cmd)115 static int dispatch_event_outcmd(struct arcan_event* ev, const char* cmd)
116 {
117 	static FILE* outf;
118 
119 	switch(ev->tgt.kind){
120 	case TARGET_COMMAND_MESSAGE:{
121 		if (!outf && !(outf = popen(cmd, "w")))
122 			return -1;
123 		write_ev(ev, outf);
124 		if (ev->tgt.ioevs[0].iv == 0){
125 			fclose(outf);
126 			outf = NULL;
127 			return 1;
128 		}
129 	}
130 	break;
131 	case TARGET_COMMAND_EXIT:
132 		return -1;
133 	break;
134 	default:
135 	break;
136 	}
137 	return 0;
138 }
139 
140 static const struct option longopts[] = {
141 	{ "help",         no_argument,       NULL, 'h'},
142 	{ "in",           no_argument,       NULL, 'i'},
143 	{ "in-data",      required_argument, NULL, 'I'},
144 	{ "out",          no_argument,       NULL, 'o'},
145 	{ "exec",         required_argument, NULL, 'e'},
146 	{ "separator",    required_argument, NULL, 'p'},
147 	{ "loop",         required_argument, NULL, 'l'},
148 	{ "display",      required_argument, NULL, 'd'},
149 	{ "silent",       no_argument,       NULL, 's'},
150 	{ NULL,           no_argument,       NULL,  0 }
151 };
152 
usage()153 static void usage()
154 {
155 printf("Usage: aclip [-hioe:s:l:d:]\n"
156 "-h    \t--help         \tthis text\n"
157 "-i    \t--in           \tread/validate UTF-8 from standard input and copy to clipboard\n"
158 "-I arg\t--in-data arg  \tread/validate UTF-8 and copy to clipboard\n"
159 "-o    \t--out          \tflush received pastes to stdout\n"
160 "-e arg\t--exec arg     \tpopen [arg] on received pastes and flush to its stdin\n"
161 "-p arg\t--separator arg\ttreat [arg] as paste- separator separator (-l >= 0)\n"
162 "-l arg\t--loop arg     \texit after [arg] discrete paste operations (-0, never exit)\n"
163 "-s    \t--silent       \tclose stdout and fork into background\n"
164 "-d arg\t--display arg  \tuse [arg] as connection path istead of ARCAN_CONNPATH env\n"
165 );
166 }
167 
main(int argc,char ** argv)168 int main(int argc, char** argv)
169 {
170 	int loop_counter = -1;
171 
172 	if (argc == 1){
173 		usage();
174 		return EXIT_FAILURE;
175 	}
176 
177 	bool use_stdin = false, use_stdout = false, silence = false;
178 	char* exec_cmd = NULL, (* copy_arg) = NULL;
179 
180 	int ch;
181 	while((ch = getopt_long(argc, argv, "hisI:oe:p:l:d:", longopts, NULL)) >= 0)
182 	switch(ch){
183 	case 'h' : usage(); return EXIT_SUCCESS;
184 	case 'i' : use_stdin = true; break;
185 	case 'o' : use_stdout = true; break;
186 	case 's' : silence = true; break;
187 	case 'I' : {
188 		if (copy_arg)
189 			free(copy_arg);
190 		copy_arg = strdup(optarg);
191 	}
192 	break;
193 	case 'e' : {
194 		if (exec_cmd)
195 			free(exec_cmd);
196 		exec_cmd = strdup(optarg);
197 	}
198 	break;
199 	case 'd' : setenv("ARCAN_CONNPATH", optarg, 1); break;
200 	case 'l' : loop_counter = strtoul(optarg, NULL, 10); break;
201 	case 'p' : separator = strdup(optarg ? optarg : ""); break;
202 	default:
203 		break;
204 	}
205 
206 	if (!use_stdin && !use_stdout && !copy_arg){
207 		fprintf(stderr, "neither [-i], [-I arg] nor [-o] specified.\n");
208 		usage();
209 		return EXIT_FAILURE;
210 	}
211 
212 /* Something of an inconvenience right now, in order to get access to a proper
213  * pasteboard (full A/V support) we need a non-output primary segment through
214  * which that request can be pushed. If we get one, we can just switch the
215  * struct that processes paste- output though. */
216 	struct arcan_shmif_cont con = arcan_shmif_open_ext(
217 		SHMIF_ACQUIRE_FATALFAIL, NULL, (struct shmif_open_ext){
218 		.type = SEGID_CLIPBOARD,
219 		.title = "",
220 		.ident = ""
221 	}, sizeof(struct shmif_open_ext));
222 	arcan_shmif_signal(&con, SHMIF_SIGVID);
223 
224 	if (signal(SIGCHLD, SIG_IGN) == SIG_ERR){
225 		fprintf(stderr, "ign on sigchld failed\n");
226 		return EXIT_FAILURE;
227 	}
228 
229 	if (silence){
230 		fclose(stdout);
231 		if (fork() != 0)
232 			return EXIT_SUCCESS;
233 	}
234 
235 /*
236  * Two little "gotcha's" here, one is that a lot of the event handler / resource
237  * allocation schemes are actually deferred until the first transfer signal.
238  * The other is a possible race: _drop pulls the 'dms' and the client is
239  * considered dead. Whatever is pending on the event queue at the moment will
240  * never be processed and our paste- message gets lost, so we want synch-
241  * delivery here. See the approach in drop_exit at the end.
242  */
243 	if (copy_arg){
244 		paste(&con, copy_arg, false);
245 	}
246 
247 /* "worst" case, both input and output - fork, block-read from stdin and put to
248  * output queue. cont can be shared in this corner case as the parent will still
249  * be alive due to the in/out event-queue separation. */
250 	bool drop_exit = true;
251 	if (use_stdin && use_stdout){
252 		pid_t pv = fork();
253 		if (pv > 0){
254 			use_stdin = false;
255 		}
256 		else if (pv == 0){
257 			use_stdout = false;
258 			drop_exit = false;
259 		}
260 	}
261 
262 /* block/flush stdin and split into paste- messages */
263 	arcan_event ev;
264 	if (use_stdin){
265 		char buf[sizeof(ev.ext.message.data) * 4];
266 		while(!feof(stdin) && !ferror(stdin)){
267 			size_t ntr = fread(buf, 1, sizeof(buf)-1, stdin);
268 			buf[ntr] = '\0';
269 			if (ntr && ntr < sizeof(buf)-1)
270 				paste(&con, buf, !(feof(stdin) || ferror(stdin)));
271 		}
272 	}
273 
274 /* normal event loop and trigger when complete messages are received */
275 	if (use_stdout){
276 		while(arcan_shmif_wait(&con, &ev) > 0){
277 			if (ev.category == EVENT_TARGET){
278 				int sv = exec_cmd ?
279 					dispatch_event_outcmd(&ev, exec_cmd) :
280 					dispatch_event_outf(&ev, stdout);
281 				if (-1 == sv || (1 == sv && loop_counter == 1))
282 					break;
283 				if (loop_counter > 0)
284 					loop_counter--;
285 			}
286 		}
287 	}
288 
289 /* defer the drop call until the eventqueue is empty, not a very pretty
290  * solution but this is a rather fringe use-case (connect, queue one
291  * event, shutdown) to play out in an asynch setting */
292 	if (drop_exit){
293 		while (con.addr && con.addr->dms &&
294 			con.addr->parentevq.front != con.addr->parentevq.back)
295 			arcan_shmif_signal(&con, SHMIF_SIGVID);
296 		arcan_shmif_drop(&con);
297 	}
298 
299 	return EXIT_SUCCESS;
300 }
301