|
43 | 43 | #include <algorithm> |
44 | 44 | #include <atomic> |
45 | 45 | #include <cctype> |
| 46 | +#include <iostream> |
| 47 | +#include <sstream> |
46 | 48 | #include <string> |
47 | 49 | #include <chrono> |
48 | 50 | #include <thread> |
@@ -163,6 +165,324 @@ static bool parse_call_id_mode(const char *value, int *mode) |
163 | 165 | return false; |
164 | 166 | } |
165 | 167 |
|
| 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 | + |
166 | 486 | /* Put each option, its help text, and type in this table. */ |
167 | 487 | struct sipp_option options_table[] = { |
168 | 488 | {"h", nullptr, SIPP_OPTION_HELP, nullptr, 0}, |
@@ -904,9 +1224,12 @@ static void help() |
904 | 1224 | "Usage:\n" |
905 | 1225 | "\n" |
906 | 1226 | " sipp remote_host[:remote_port] [options]\n" |
| 1227 | + " sipp\n" |
907 | 1228 | "\n" |
908 | 1229 | "Example:\n" |
909 | 1230 | "\n" |
| 1231 | + " Launch the interactive startup wizard:\n" |
| 1232 | + " ./sipp\n" |
910 | 1233 | " Run SIPp with embedded server (uas) scenario:\n" |
911 | 1234 | " ./sipp -sn uas\n" |
912 | 1235 | " On the same host, run SIPp with embedded client (uac) scenario:\n" |
@@ -1401,17 +1724,32 @@ int main(int argc, char *argv[]) |
1401 | 1724 | bool slave_masterSet = false; |
1402 | 1725 | int rtp_errors; |
1403 | 1726 | int echo_errors; |
| 1727 | + std::vector<std::string> wizard_args_storage; |
| 1728 | + std::vector<char *> wizard_argv; |
1404 | 1729 |
|
1405 | 1730 | rtp_errors = 0; |
1406 | 1731 | echo_errors = 0; |
1407 | 1732 |
|
1408 | 1733 | randomseed(); |
1409 | 1734 |
|
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) { |
1412 | 1749 | help(); |
1413 | 1750 | exit(EXIT_OTHER); |
1414 | 1751 | } |
| 1752 | + |
1415 | 1753 | { |
1416 | 1754 | /* Ignore the SIGPIPE signal */ |
1417 | 1755 | struct sigaction action_pipe; |
|
0 commit comments