Skip to content

Commit 278362b

Browse files
darwvin-devorgads
authored andcommitted
feat: add interactive startup wizard
1 parent 31658c2 commit 278362b

1 file changed

Lines changed: 340 additions & 2 deletions

File tree

src/sipp.cpp

Lines changed: 340 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
#include <algorithm>
4444
#include <atomic>
4545
#include <cctype>
46+
#include <iostream>
47+
#include <sstream>
4648
#include <string>
4749
#include <chrono>
4850
#include <thread>
@@ -163,6 +165,324 @@ static bool parse_call_id_mode(const char *value, int *mode)
163165
return false;
164166
}
165167

168+
struct wizard_scenario_option {
169+
const char *scenario_name;
170+
const char *description;
171+
bool needs_remote_host;
172+
bool use_scenario_file;
173+
};
174+
175+
static const wizard_scenario_option wizard_scenarios[] = {
176+
{"uac", "Embedded UAC client scenario.", true, false},
177+
{"uas", "Embedded UAS server scenario.", false, false},
178+
{"regexp", "Embedded UAC scenario with regexp and variables.", true, false},
179+
{"branchc", "Embedded client branching scenario.", true, false},
180+
{"branchs", "Embedded server branching scenario.", false, false},
181+
{"uac_pcap", "Embedded UAC scenario with PCAP media playback.", true, false},
182+
{"custom", "Custom XML scenario file.", false, true},
183+
};
184+
185+
static std::string trim_copy(const std::string &value)
186+
{
187+
size_t first = value.find_first_not_of(" \t\r\n");
188+
if (first == std::string::npos) {
189+
return "";
190+
}
191+
192+
size_t last = value.find_last_not_of(" \t\r\n");
193+
return value.substr(first, last - first + 1);
194+
}
195+
196+
static bool wizard_cancelled(const std::string &value)
197+
{
198+
std::string lowered = lowercase_copy(trim_copy(value));
199+
return lowered == "q" || lowered == "quit" || lowered == "exit";
200+
}
201+
202+
static bool wizard_prompt_line(const std::string &prompt, std::string *result, const char *default_value = nullptr)
203+
{
204+
std::cout << prompt;
205+
std::cout.flush();
206+
207+
/* Treat EOF the same as cancellation so non-interactive callers stop cleanly. */
208+
if (!std::getline(std::cin, *result)) {
209+
std::cout << "\n";
210+
return false;
211+
}
212+
213+
/* Normalize every answer before testing cancellation or applying defaults. */
214+
*result = trim_copy(*result);
215+
if (wizard_cancelled(*result)) {
216+
return false;
217+
}
218+
219+
if (result->empty() && default_value) {
220+
*result = default_value;
221+
}
222+
223+
return true;
224+
}
225+
226+
static bool parse_transport_choice(const std::string &value, std::string *transport_arg)
227+
{
228+
std::string lowered = lowercase_copy(trim_copy(value));
229+
230+
/* Convert friendly names into the existing -t option values. */
231+
if (lowered == "udp" || lowered == "u1") {
232+
*transport_arg = "u1";
233+
return true;
234+
}
235+
if (lowered == "tcp" || lowered == "t1") {
236+
*transport_arg = "t1";
237+
return true;
238+
}
239+
if (lowered == "tls" || lowered == "l1") {
240+
*transport_arg = "l1";
241+
return true;
242+
}
243+
#ifdef USE_SCTP
244+
if (lowered == "sctp" || lowered == "s1") {
245+
*transport_arg = "s1";
246+
return true;
247+
}
248+
#endif
249+
250+
return false;
251+
}
252+
253+
static std::vector<std::string> split_simple_args(const std::string &input)
254+
{
255+
std::vector<std::string> result;
256+
std::istringstream words(input);
257+
std::string word;
258+
259+
/* Keep parsing intentionally simple: this mirrors a shell-style word list. */
260+
while (words >> word) {
261+
result.push_back(word);
262+
}
263+
264+
return result;
265+
}
266+
267+
static bool should_launch_startup_wizard(int argc)
268+
{
269+
/* Only interrupt bare interactive launches; scripted invocations keep argv intact. */
270+
return argc < 2 && isatty(STDIN_FILENO) && isatty(STDOUT_FILENO);
271+
}
272+
273+
/* Collect interactive answers and translate them into normal SIPp command options. */
274+
static std::vector<std::string> launch_startup_wizard(const char *program_name)
275+
{
276+
std::vector<std::string> args;
277+
const wizard_scenario_option *scenario_choice = nullptr;
278+
std::string input;
279+
std::string transport_arg = "u1";
280+
std::string scenario_path;
281+
std::string remote_host_value;
282+
std::string service_value;
283+
std::string rate_value;
284+
std::string max_calls_value;
285+
std::string concurrent_calls_value;
286+
std::string extra_args;
287+
bool custom_scenario_uses_remote_host = false;
288+
289+
std::cout
290+
<< "\nSIPp startup wizard\n"
291+
<< "Press Enter to accept defaults. Type 'q' to quit.\n\n";
292+
293+
/* Choose the scenario first because later prompts depend on its role. */
294+
while (!scenario_choice) {
295+
std::cout << "Scenario:\n";
296+
for (size_t i = 0; i < sizeof(wizard_scenarios) / sizeof(wizard_scenarios[0]); ++i) {
297+
std::cout << " " << (i + 1) << ") " << wizard_scenarios[i].scenario_name
298+
<< " - " << wizard_scenarios[i].description << "\n";
299+
}
300+
std::cout << "\n";
301+
302+
if (!wizard_prompt_line("Choose a scenario [1]: ", &input, "1")) {
303+
return {};
304+
}
305+
306+
std::string lowered = lowercase_copy(input);
307+
size_t numeric_choice = 0;
308+
/* Accept either the displayed number or the scenario name. */
309+
if (!lowered.empty() &&
310+
std::all_of(lowered.begin(), lowered.end(),
311+
[](unsigned char c) { return std::isdigit(c) != 0; })) {
312+
numeric_choice = static_cast<size_t>(strtoul(lowered.c_str(), nullptr, 10));
313+
}
314+
315+
if (numeric_choice >= 1 &&
316+
numeric_choice <= (sizeof(wizard_scenarios) / sizeof(wizard_scenarios[0]))) {
317+
scenario_choice = &wizard_scenarios[numeric_choice - 1];
318+
break;
319+
}
320+
321+
for (const wizard_scenario_option &candidate : wizard_scenarios) {
322+
if (lowered == lowercase_copy(candidate.scenario_name)) {
323+
scenario_choice = &candidate;
324+
break;
325+
}
326+
}
327+
328+
if (!scenario_choice) {
329+
std::cout << "Unknown scenario choice. Please select one of the listed items.\n\n";
330+
}
331+
}
332+
333+
/* File-based scenarios need a readable XML path and may still be client-side. */
334+
if (scenario_choice->use_scenario_file) {
335+
while (true) {
336+
if (!wizard_prompt_line("Path to XML scenario file: ", &scenario_path)) {
337+
return {};
338+
}
339+
if (scenario_path.empty()) {
340+
std::cout << "A scenario path is required.\n";
341+
continue;
342+
}
343+
if (access(scenario_path.c_str(), R_OK) != 0) {
344+
std::cout << "Unable to read '" << scenario_path << "'. Try another path.\n";
345+
continue;
346+
}
347+
break;
348+
}
349+
350+
while (true) {
351+
if (!wizard_prompt_line("Does this scenario send calls to a remote host? [y/N]: ",
352+
&input, "n")) {
353+
return {};
354+
}
355+
356+
/* A custom XML scenario can be either client-side or server-side. */
357+
std::string lowered = lowercase_copy(input);
358+
if (lowered == "y" || lowered == "yes") {
359+
custom_scenario_uses_remote_host = true;
360+
break;
361+
}
362+
if (lowered == "n" || lowered == "no") {
363+
custom_scenario_uses_remote_host = false;
364+
break;
365+
}
366+
367+
std::cout << "Please answer y or n.\n";
368+
}
369+
}
370+
371+
/* Client scenarios need a target host; server-style scenarios can bind locally. */
372+
if (scenario_choice->needs_remote_host || custom_scenario_uses_remote_host) {
373+
while (remote_host_value.empty()) {
374+
if (!wizard_prompt_line("Remote host[:port] [127.0.0.1]: ", &remote_host_value, "127.0.0.1")) {
375+
return {};
376+
}
377+
if (remote_host_value.empty()) {
378+
std::cout << "A remote host is required for this scenario.\n";
379+
}
380+
}
381+
}
382+
383+
/* Keep the transport choices aligned with the features compiled into this build. */
384+
while (true) {
385+
#ifdef USE_SCTP
386+
const char *transport_prompt = "Transport [udp/tcp/tls/sctp] (default udp): ";
387+
#else
388+
const char *transport_prompt = "Transport [udp/tcp/tls] (default udp): ";
389+
#endif
390+
if (!wizard_prompt_line(transport_prompt, &input, "udp")) {
391+
return {};
392+
}
393+
if (parse_transport_choice(input, &transport_arg)) {
394+
break;
395+
}
396+
std::cout << "Unknown transport. Use udp, tcp, tls";
397+
#ifdef USE_SCTP
398+
std::cout << ", or sctp";
399+
#endif
400+
std::cout << ".\n";
401+
}
402+
403+
/* Built-in client scenarios use -s for the request URI user. */
404+
if (scenario_choice->needs_remote_host) {
405+
if (!wizard_prompt_line("Request URI user (-s) [service]: ", &service_value, DEFAULT_SERVICE)) {
406+
return {};
407+
}
408+
}
409+
410+
/* Load-generation limits are only useful once SIPp is placing outbound calls. */
411+
if (scenario_choice->needs_remote_host || custom_scenario_uses_remote_host) {
412+
if (!wizard_prompt_line("Call rate (-r) [10]: ", &rate_value, "10")) {
413+
return {};
414+
}
415+
if (!wizard_prompt_line("Max calls (-m, blank for unlimited): ", &max_calls_value)) {
416+
return {};
417+
}
418+
if (!wizard_prompt_line("Max simultaneous calls (-l, optional): ", &concurrent_calls_value)) {
419+
return {};
420+
}
421+
}
422+
423+
if (!wizard_prompt_line("Extra SIPp options (optional, space-separated): ", &extra_args)) {
424+
return {};
425+
}
426+
427+
/* Render the collected answers back into the same argv shape normal parsing uses. */
428+
args.push_back(program_name);
429+
if (scenario_choice->use_scenario_file) {
430+
args.push_back("-sf");
431+
args.push_back(scenario_path);
432+
} else {
433+
args.push_back("-sn");
434+
args.push_back(scenario_choice->scenario_name);
435+
}
436+
437+
args.push_back("-t");
438+
args.push_back(transport_arg);
439+
440+
if (!service_value.empty()) {
441+
args.push_back("-s");
442+
args.push_back(service_value);
443+
}
444+
if (!rate_value.empty()) {
445+
args.push_back("-r");
446+
args.push_back(rate_value);
447+
}
448+
if (!max_calls_value.empty()) {
449+
args.push_back("-m");
450+
args.push_back(max_calls_value);
451+
}
452+
if (!concurrent_calls_value.empty()) {
453+
args.push_back("-l");
454+
args.push_back(concurrent_calls_value);
455+
}
456+
if (!remote_host_value.empty()) {
457+
args.push_back(remote_host_value);
458+
}
459+
460+
/* Preserve advanced options without trying to duplicate the main option parser here. */
461+
std::vector<std::string> extra_words = split_simple_args(extra_args);
462+
args.insert(args.end(), extra_words.begin(), extra_words.end());
463+
464+
std::cout << "\nCommand:\n ";
465+
for (size_t i = 0; i < args.size(); ++i) {
466+
if (i != 0) {
467+
std::cout << " ";
468+
}
469+
std::cout << args[i];
470+
}
471+
std::cout << "\n\n";
472+
473+
/* Let the user review the exact command before SIPp continues startup. */
474+
if (!wizard_prompt_line("Run this command now? [Y/n]: ", &input, "y")) {
475+
return {};
476+
}
477+
std::string lowered = lowercase_copy(input);
478+
if (!(lowered == "y" || lowered == "yes")) {
479+
std::cout << "Wizard cancelled.\n";
480+
return {};
481+
}
482+
483+
return args;
484+
}
485+
166486
/* Put each option, its help text, and type in this table. */
167487
struct sipp_option options_table[] = {
168488
{"h", nullptr, SIPP_OPTION_HELP, nullptr, 0},
@@ -904,9 +1224,12 @@ static void help()
9041224
"Usage:\n"
9051225
"\n"
9061226
" sipp remote_host[:remote_port] [options]\n"
1227+
" sipp\n"
9071228
"\n"
9081229
"Example:\n"
9091230
"\n"
1231+
" Launch the interactive startup wizard:\n"
1232+
" ./sipp\n"
9101233
" Run SIPp with embedded server (uas) scenario:\n"
9111234
" ./sipp -sn uas\n"
9121235
" On the same host, run SIPp with embedded client (uac) scenario:\n"
@@ -1401,17 +1724,32 @@ int main(int argc, char *argv[])
14011724
bool slave_masterSet = false;
14021725
int rtp_errors;
14031726
int echo_errors;
1727+
std::vector<std::string> wizard_args_storage;
1728+
std::vector<char *> wizard_argv;
14041729

14051730
rtp_errors = 0;
14061731
echo_errors = 0;
14071732

14081733
randomseed();
14091734

1410-
/* At least one argument is needed */
1411-
if (argc < 2) {
1735+
if (should_launch_startup_wizard(argc)) {
1736+
wizard_args_storage = launch_startup_wizard(argv[0]);
1737+
if (wizard_args_storage.empty()) {
1738+
exit(EXIT_OTHER);
1739+
}
1740+
1741+
wizard_argv.reserve(wizard_args_storage.size());
1742+
for (std::string &value : wizard_args_storage) {
1743+
wizard_argv.push_back(value.data());
1744+
}
1745+
1746+
argc = static_cast<int>(wizard_argv.size());
1747+
argv = wizard_argv.data();
1748+
} else if (argc < 2) {
14121749
help();
14131750
exit(EXIT_OTHER);
14141751
}
1752+
14151753
{
14161754
/* Ignore the SIGPIPE signal */
14171755
struct sigaction action_pipe;

0 commit comments

Comments
 (0)