/[svn]/linuxsampler/trunk/src/shell/lscp.cpp
ViewVC logotype

Contents of /linuxsampler/trunk/src/shell/lscp.cpp

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2532 - (show annotations) (download)
Wed Mar 5 17:29:15 2014 UTC (10 years, 1 month ago) by schoenebeck
File size: 19372 byte(s)
* LSCP server: fixed crash caused by endless recursion in
  LSCP shell grammar evaluation algorithm.
* LSCP shell: quit shell app when TCP connection aborted.
* Bumped version (1.0.0.svn36).

1 /*
2 * LSCP Shell
3 *
4 * Copyright (c) 2014 Christian Schoenebeck
5 *
6 * This program is part of LinuxSampler and released under the same terms.
7 */
8
9 #include <stdio.h>
10 #include <stdlib.h>
11 #include <iostream>
12 #include <sstream>
13 #include <string.h>
14
15 #include "LSCPClient.h"
16 #include "KeyboardReader.h"
17 #include "TerminalCtrl.h"
18 #include "CFmt.h"
19 #include "CCursor.h"
20 #include "TerminalPrinter.h"
21
22 #include "../common/global.h"
23 #include "../common/global_private.h"
24 #include "../common/Condition.h"
25
26 #define LSCP_DEFAULT_HOST "localhost"
27 #define LSCP_DEFAULT_PORT 8888
28
29 using namespace std;
30 using namespace LinuxSampler;
31
32 static LSCPClient* g_client = NULL;
33 static KeyboardReader* g_keyboardReader = NULL;
34 static Condition g_todo;
35 static String g_goodPortion;
36 static String g_badPortion;
37 static String g_suggestedPortion;
38 static int g_linesActive = 0;
39 static const String g_prompt = "lscp=# ";
40 static std::vector<String> g_commandHistory;
41 static int g_commandHistoryIndex = -1;
42
43 static void printUsage() {
44 cout << "lscp - The LinuxSampler Control Protocol (LSCP) Shell." << endl;
45 cout << endl;
46 cout << "Usage: lscp [-h HOSTNAME] [-p PORT]" << endl;
47 cout << endl;
48 cout << " -h Host name of LSCP server (default \"" << LSCP_DEFAULT_HOST << "\")." << endl;
49 cout << endl;
50 cout << " -p TCP port number of LSCP server (default " << LSCP_DEFAULT_PORT << ")." << endl;
51 cout << endl;
52 cout << " --no-auto-correct Don't perform auto correction of obvious syntax errors." << endl;
53 cout << endl;
54 }
55
56 static void printWelcome() {
57 cout << "Welcome to lscp " << VERSION << ", the LinuxSampler Control Protocol (LSCP) shell." << endl;
58 cout << endl;
59 }
60
61 static void printPrompt() {
62 cout << g_prompt << flush;
63 }
64
65 static int promptOffset() {
66 return g_prompt.size();
67 }
68
69 static void quitApp(int code = 0) {
70 std::cout << std::endl << std::flush;
71 if (g_client) delete g_client;
72 if (g_keyboardReader) delete g_keyboardReader;
73 exit(code);
74 }
75
76 // Called by the network reading thread, whenever new data arrived from the
77 // network connection.
78 static void onLSCPClientNewInputAvailable(LSCPClient* client) {
79 g_todo.Set(true);
80 }
81
82 // Called by the keyboard reading thread, whenever a key stroke was received.
83 static void onNewKeyboardInputAvailable(KeyboardReader* reader) {
84 g_todo.Set(true);
85 }
86
87 // Called on network error or when server side closed the TCP connection.
88 static void onLSCPClientErrorOccured(LSCPClient* client) {
89 quitApp();
90 }
91
92 /// Will be called when the user hits tab key to trigger auto completion.
93 static void autoComplete() {
94 if (g_suggestedPortion.empty()) return;
95 String s;
96 // let the server delete mistaken characters first
97 for (int i = 0; i < g_badPortion.size(); ++i) s += '\b';
98 // now add the suggested, correct characters
99 s += g_suggestedPortion;
100 g_suggestedPortion.clear();
101 g_client->send(s);
102 }
103
104 static void commandFromHistory(int offset) {
105 if (g_commandHistoryIndex + offset < 0 ||
106 g_commandHistoryIndex + offset >= g_commandHistory.size()) return;
107 g_commandHistoryIndex += offset;
108 int len = g_goodPortion.size() + g_badPortion.size();
109 // erase current active line
110 for (int i = 0; i < len; ++i) g_client->send('\b');
111 // transmit new/old line to LSCP server
112 String command = g_commandHistory[g_commandHistory.size() - g_commandHistoryIndex - 1];
113 g_client->send(command);
114 }
115
116 /// Will be called when the user hits arrow up key, to iterate to an older command line.
117 static void previousCommand() {
118 commandFromHistory(1);
119 }
120
121 /// Will be called when the user hits arrow down key, to iterate to a more recent command line.
122 static void nextCommand() {
123 commandFromHistory(-1);
124 }
125
126 /// Will be called whenever the user hits ENTER, to store the line in the command history.
127 static void storeCommandInHistory(const String& sCommand) {
128 g_commandHistoryIndex = -1; // reset history index
129 // don't save the command if the previous one was the same
130 if (g_commandHistory.empty() || g_commandHistory.back() != sCommand)
131 g_commandHistory.push_back(sCommand);
132 }
133
134 /**
135 * This LSCP shell application is designed as thin client. That means the heavy
136 * LSCP grammar evaluation tasks are peformed by the LSCP server and the shell
137 * application's task are simply limited to forward individual characters typed
138 * by the user to the LSCP server and showing the result of the LSCP server's
139 * evaluation to the user on the screen. This has the big advantage that the
140 * shell works perfectly with any machine running (some minimum recent version
141 * of) LinuxSampler, no matter which precise LSCP version the server side
142 * is using. Which reduces the maintenance efforts for the shell application
143 * development tremendously.
144 *
145 * As soon as this application established a TCP connection to a LSCP server, it
146 * sends this command to the LSCP server:
147 * @code
148 * SET SHELL INTERACT 1
149 * @endcode
150 * which will inform the LSCP server that this LSCP client is actually a LSCP
151 * shell application. The shell will then simply forward every single character
152 * typed by the user immediately to the LSCP server. The LSCP server in turn
153 * will evaluate every single character received and will return immediately a
154 * specially formatted string to the shell application like (assuming the user's
155 * current command line was "CREATE AUasdf"):
156 * @code
157 * SHU:1:CREATE AU{{GF}}asdf{{CU}}{{SB}}DIO_OUTPUT_DEVICE
158 * @endcode
159 * which informs this shell application about the result of the LSCP grammar
160 * evaluation and allows the shell to easily show that result of the evaluation
161 * to the user on the screen. In the example reply above, the prefix "SHU:" just
162 * indicates to the shell application that this response line is the result
163 * of the latest grammar evaluation, the number followed (here 1) indicates the
164 * semantic status of the current command line:
165 *
166 * - 0: Command line is complete, thus ENTER key may be hit by the user now.
167 * - 1: Current command line contains syntax error(s).
168 * - 2: Command line is incomplete, but contains no syntax errors so far.
169 *
170 * Then the actual current command line follows, with special markers:
171 *
172 * - Left of "{{GF}}" the command line is syntactically correct, right of that
173 * marker the command line is syntactically wrong.
174 *
175 * - Marker "{{CU}}" indicates the current cursor position of the command line.
176 *
177 * - Right of "{{SB}}" follows the current auto completion suggestion, so that
178 * string portion was not typed by the user yet, but is expected to be typed
179 * by him next, to retain syntax correctness. The auto completion portion is
180 * added by the LSCP server only if there is one unique way to add characters
181 * to the current command line. If there are multiple possibilities, than this
182 * portion is missing due to ambiguity.
183 *
184 * - Optionally there might also be a "{{PB}" marker on right hand side of the
185 * line. The text right to that marker reflects all possibilities at the
186 * user's current input position (which cannot be auto completed) due to
187 * ambiguity, including abstract (a.k.a. "non-terminal") symbols like:
188 * @code
189 * <digit>, <text>, <number>, etc.
190 * @endcode
191 * This portion is added by the LSCP server only if there is not a unique way
192 * to add characters to the current command line.
193 */
194 int main(int argc, char *argv[]) {
195 String host = LSCP_DEFAULT_HOST;
196 int port = LSCP_DEFAULT_PORT;
197 bool autoCorrect = true;
198
199 // parse command line arguments
200 for (int i = 0; i < argc; ++i) {
201 String s = argv[i];
202 if (s == "-h" || s == "--host") {
203 if (++i >= argc) {
204 printUsage();
205 return -1;
206 }
207 host = argv[i];
208 } else if (s == "-p" || s == "--port") {
209 if (++i >= argc) {
210 printUsage();
211 return -1;
212 }
213 port = atoi(argv[i]);
214 if (port <= 0) {
215 cerr << "Error: invalid port argument \"" << argv[i] << "\"\n";
216 return -1;
217 }
218 } else if (s == "--no-auto-correct") {
219 autoCorrect = false;
220 } else if (s[0] == '-') { // invalid / unknown command line argument ...
221 printUsage();
222 return -1;
223 }
224 }
225
226 // try to connect to the sampler's LSCP server and start a thread for
227 // receiving incoming network data from the sampler's LSCP server
228 g_client = new LSCPClient;
229 g_client->setCallback(onLSCPClientNewInputAvailable);
230 g_client->setErrorCallback(onLSCPClientErrorOccured);
231 if (!g_client->connect(host, port)) return -1;
232 String sResponse = g_client->sendCommandSync(
233 (autoCorrect) ? "SET SHELL AUTO_CORRECT 1" : "SET SHELL AUTO_CORRECT 0"
234 );
235 sResponse = g_client->sendCommandSync("SET SHELL INTERACT 1");
236 if (sResponse.substr(0, 2) != "OK") {
237 cerr << "Error: sampler too old, it does not support shell instructions\n";
238 return -1;
239 }
240
241 printWelcome();
242 printPrompt();
243
244 // start a thread for reading from the local text input keyboard
245 // (keyboard echo will be disabled as well to have a clean control on what
246 // is appearing on the screen)
247 g_keyboardReader = new KeyboardReader;
248 g_keyboardReader->setCallback(onNewKeyboardInputAvailable);
249 g_keyboardReader->startReading();
250
251 int iKbdEscapeCharsExpected = 0;
252 char kbdPrevEscapeChar;
253
254 // main thread's loop
255 //
256 // This application runs 3 threads:
257 //
258 // - Keyboard thread: reads constantly on stdin for new characters (which
259 // will block this keyboard thread until new character(s) were typed by
260 // the user) and pushes the typed characters into a FIFO buffer.
261 //
262 // - Network thread: reads constantly on the TCP connection for new bytes
263 // being sent by the LSCP server (which will block this network thread
264 // until new bytes were received) and pushes the received bytes into a
265 // FIFO buffer.
266 //
267 // - Main thread: this thread runs in the loop below. The main thread sleeps
268 // (by using the "g_todo" condition variable) until either new keys on the
269 // keyboard were stroke by the user or until new bytes were received from
270 // the LSCP server. The main thread will then accordingly send the typed
271 // characters to the LSCP server and/or show the result of the LSCP
272 // server's latest evaluation to the user on the screen (by pulling those
273 // data from the other two thread's FIFO buffers).
274 while (true) {
275 // sleep until either new data from the network or from keyboard arrived
276 g_todo.WaitIf(false);
277 // immediately unset the condition variable and unlock it
278 g_todo.Set(false);
279 g_todo.Unlock();
280
281 // did network data arrive?
282 while (g_client->messageComplete()) {
283 String line = *g_client->popLine();
284 //printf("line '%s'\n", line.c_str());
285 if (line.substr(0,4) == "SHU:") {
286 int code = 0, n = 0;
287 int res = sscanf(line.c_str(), "SHU:%d:%n", &code, &n);
288 if (res >= 1) {
289 String s = line.substr(n);
290
291 // extract portion that is already syntactically correct
292 size_t iGood = s.find(LSCP_SHK_GOOD_FRONT);
293 String sGood = s.substr(0, iGood);
294 if (sGood.find(LSCP_SHK_CURSOR) != string::npos)
295 sGood.erase(sGood.find(LSCP_SHK_CURSOR), strlen(LSCP_SHK_CURSOR)); // erase cursor marker
296
297 // extract portion that was written syntactically incorrect
298 String sBad = s.substr(iGood + strlen(LSCP_SHK_GOOD_FRONT));
299 if (sBad.find(LSCP_SHK_CURSOR) != string::npos)
300 sBad.erase(sBad.find(LSCP_SHK_CURSOR), strlen(LSCP_SHK_CURSOR)); // erase cursor marker
301 if (sBad.find(LSCP_SHK_SUGGEST_BACK) != string::npos)
302 sBad.erase(sBad.find(LSCP_SHK_SUGGEST_BACK)); // erase auto suggestion portion
303 if (sBad.find(LSCP_SHK_POSSIBILITIES_BACK) != string::npos)
304 sBad.erase(sBad.find(LSCP_SHK_POSSIBILITIES_BACK)); // erase possibilities portion
305
306 // extract portion that is suggested for auto completion
307 String sSuggest;
308 if (s.find(LSCP_SHK_SUGGEST_BACK) != string::npos) {
309 sSuggest = s.substr(s.find(LSCP_SHK_SUGGEST_BACK) + strlen(LSCP_SHK_SUGGEST_BACK));
310 if (sSuggest.find(LSCP_SHK_CURSOR) != string::npos)
311 sSuggest.erase(sSuggest.find(LSCP_SHK_CURSOR), strlen(LSCP_SHK_CURSOR)); // erase cursor marker
312 if (sSuggest.find(LSCP_SHK_POSSIBILITIES_BACK) != string::npos)
313 sSuggest.erase(sSuggest.find(LSCP_SHK_POSSIBILITIES_BACK)); // erase possibilities portion
314 }
315
316 // extract portion that provides all current possibilities
317 // (that is all branches in the current grammar tree)
318 String sPossibilities;
319 if (s.find(LSCP_SHK_POSSIBILITIES_BACK) != string::npos) {
320 sPossibilities = s.substr(s.find(LSCP_SHK_POSSIBILITIES_BACK) + strlen(LSCP_SHK_POSSIBILITIES_BACK));
321 }
322
323 // extract current cursor position
324 int cursorColumn = sGood.size();
325 String sCursor = s;
326 if (sCursor.find(LSCP_SHK_GOOD_FRONT) != string::npos)
327 sCursor.erase(sCursor.find(LSCP_SHK_GOOD_FRONT), strlen(LSCP_SHK_GOOD_FRONT)); // erase good/bad marker
328 if (sCursor.find(LSCP_SHK_SUGGEST_BACK) != string::npos)
329 sCursor.erase(sCursor.find(LSCP_SHK_SUGGEST_BACK), strlen(LSCP_SHK_SUGGEST_BACK)); // erase suggestion marker
330 if (sCursor.find(LSCP_SHK_CURSOR) != string::npos)
331 cursorColumn = sCursor.find(LSCP_SHK_CURSOR);
332
333 // store those informations globally for the auto-completion
334 // feature
335 g_goodPortion = sGood;
336 g_badPortion = sBad;
337 g_suggestedPortion = sSuggest;
338
339 //printf("line '%s' good='%s' bad='%s' suggested='%s' cursor=%d\n", line.c_str(), sGood.c_str(), sBad.c_str(), sSuggest.c_str(), cursorColumn);
340
341 // clear current command line on screen
342 // (which may have been printed over several lines)
343 CCursor cursor = CCursor::now().toColumn(0).clearLine();
344 for (int i = 0; i < g_linesActive; ++i)
345 cursor = cursor.down().clearLine();
346 if (g_linesActive) cursor = cursor.up(g_linesActive).toColumn(0);
347 printPrompt();
348
349 // print out the gathered informations on the screen
350 TerminalPrinter p;
351 CFmt cfmt;
352 if (code == LSCP_SHU_COMPLETE) cfmt.bold().green();
353 else cfmt.bold().white();
354 p << sGood;
355 cfmt.reset().red();
356 p << sBad;
357 cfmt.bold().yellow();
358 p << sSuggest;
359 if (!sPossibilities.empty())
360 p << " <- " << sPossibilities;
361
362 // move cursor back to the appropriate input position in
363 // the command line (which may be several lines above)
364 g_linesActive = p.linesAdvanced();
365 if (p.linesAdvanced()) cursor.up(p.linesAdvanced());
366 cursor.toColumn(cursorColumn + promptOffset());
367 }
368 } else if (line.substr(0,2) == "OK") { // single-line response expected ...
369 cout << endl << flush;
370 CFmt cfmt;
371 cfmt.green();
372 cout << line.substr(0,2) << flush;
373 cfmt.reset();
374 cout << line.substr(2) << endl << flush;
375 printPrompt();
376 } else if (line.substr(0,3) == "WRN") { // single-line response expected ...
377 cout << endl << flush;
378 CFmt cfmt;
379 cfmt.yellow();
380 cout << line.substr(0,3) << flush;
381 cfmt.reset();
382 cout << line.substr(3) << endl << flush;
383 printPrompt();
384 } else if (line.substr(0,3) == "ERR") { // single-line response expected ...
385 cout << endl << flush;
386 CFmt cfmt;
387 cfmt.bold().red();
388 cout << line.substr(0,3) << flush;
389 cfmt.reset();
390 cout << line.substr(3) << endl << flush;
391 printPrompt();
392 } else if (g_client->multiLine()) { // multi-line response expected ...
393 cout << endl << flush;
394 while (true) {
395 cout << line << endl << flush;
396 if (line.substr(0, 1) == ".") break;
397 if (!g_client->lineAvailable()) break;
398 line = *g_client->popLine();
399 }
400 printPrompt();
401 } else {
402 cout << endl << line << endl << flush;
403 printPrompt();
404 }
405 }
406
407 // did keyboard input arrive?
408 while (g_keyboardReader->charAvailable()) {
409 char c = g_keyboardReader->popChar();
410
411 //std::cout << c << "(" << int(c) << ")" << std::endl << std::flush;
412 if (iKbdEscapeCharsExpected) { // escape sequence (still) expected now ...
413 iKbdEscapeCharsExpected--;
414 if (iKbdEscapeCharsExpected) kbdPrevEscapeChar = c;
415 else { // escape sequence is complete ...
416 if (kbdPrevEscapeChar == 91 && c == 65) // up key
417 previousCommand();
418 else if (kbdPrevEscapeChar == 91 && c == 66) // down key
419 nextCommand();
420 else if (kbdPrevEscapeChar == 91 && c == 68) // left key
421 g_client->send(2); // custom usage of this ASCII code
422 else if (kbdPrevEscapeChar == 91 && c == 67) // right key
423 g_client->send(3); // custom usage of this ASCII code
424 }
425 continue; // don't send this escape sequence character to LSCP server
426 } else if (c == KBD_ESCAPE) { // escape sequence for special keys expected next ...
427 iKbdEscapeCharsExpected = 2;
428 continue; // don't send ESC character to LSCP server
429 } else if (c == KBD_BACKSPACE) {
430 c = '\b';
431 } else if (c == '\t') { // auto completion ...
432 autoComplete();
433 continue; // don't send tab character to LSCP server
434 } else if (c == '\n') {
435 storeCommandInHistory(g_goodPortion + g_badPortion);
436 }
437
438 g_client->send(c);
439 }
440 }
441
442 return 0;
443 }

  ViewVC Help
Powered by ViewVC