Skip to main content
Version: 0.9.0 (next)

Local App Onboarding

Source: qar-streaming-c/examples/local_app_onboarding.c. Conceptual background: Onboarding and Sessions.

Local App Onboarding

What you build​

The smallest source app that joins QAROS on the Hub machine. It loads the installed runtime, asks the Hub to let it in, and exits with a code that says what happened. The first time, QAROS asks the user to allow the app; later starts of the same executable join silently.

This is how a source app on the Hub machine joins QAROS. There is nothing to type and no onboarding id to store. An app on another machine pairs with a code instead (Onboarding and Rejoin).

What you will learn:

  • how QarOnboardLocalAppExt joins, and what the user sees during the approval
  • why a local app always loads the installed runtime
  • how to follow the approval wait through progress messages
  • which status codes a refusal comes with, and what to tell the user for each

Before you start​

  • Work through Dynamic Loading first: this program loads the runtime the same way.
  • Windows, with QAROS installed on the machine the app runs on. When the Hub is not running, joining as a local app starts it, and the Hub keeps running after the app exits. Elsewhere the call fails with QAR_STATUS_NOT_IMPLEMENTED.
  • Someone at the Hub machine to click Allow on the first run. The prompt waits up to 120 seconds by default.

Run it​

 .\build\x64-windows\qar-streaming-c\examples\Debug\local_app_onboarding.exe
 
 # Wait up to 90 s for the approval instead of the Hub's default.
 .\build\x64-windows\qar-streaming-c\examples\Debug\local_app_onboarding.exe 90000

The only argument is the approval timeout in milliseconds. Absent or 0 means the Hub's default of 120 s; the Hub caps longer waits at 10 minutes. The program takes no library path: like a shipped app, it always runs the default lookup.

On the first run, the expected output while the approval prompt is open and after the user clicks Allow is this. The lines before Joined QAROS. are the runtime's own progress messages:

 Connecting to the local QAROS hub.
 Waiting for approval in QAROS hub
 Certificate signed.
 Onboarding complete.
 Joined QAROS.
 qar_library_destroy: SUCCESS (code=0)

Later starts of the same executable print the same lines without Waiting for approval in QAROS hub.

Exit codes, for scripts:

CodeStatusMeaning
0joined
1any otherother error, or an approval timeout that is not a number
2no runtime loaded: QAROS is not installed, or its runtime could not be loaded
3QAR_STATUS_ONBOARDING_HUB_UNREACHABLE (1802)no Hub answers on this machine, and none could be started
4QAR_STATUS_ONBOARDING_APPROVAL_DENIED (1805)Deny, a closed prompt, or a revoked approval
5QAR_STATUS_ONBOARDING_APPROVAL_TIMEOUT (1806)nobody answered in time
6QAR_STATUS_ONBOARDING_HUB_NOT_AUTHENTIC (1807)the program answering on this machine is not the installed QAROS
7QAR_STATUS_ONBOARDING_APP_IDENTITY_MISMATCH (1808)the executable no longer matches its approval

Walkthrough​

1. Read the approval timeout​

 /** \brief Parse the optional approval timeout in milliseconds. Absent or 0
  * asks for the Hub's default. Returns false for text that is not a whole
  * number of milliseconds that fits in 32 bits. */
 static bool
 parse_approval_timeout(const char* text, uint32_t* out_timeout_ms)
 {
  *out_timeout_ms = 0;
  if(text == NULL)
  {
  return true;
  }
  /* strtoul alone reads "soon" as 0 (the Hub's default) and "-1" as a huge
  * number, so check that the whole text is digits and in range. */
  if(text[0] < '0' || text[0] > '9')
  {
  return false;
  }
  char* end = NULL;
  errno = 0;
  const unsigned long long value = strtoull(text, &end, 10);
  if(errno != 0 || *end != '\0' || value > UINT32_MAX)
  {
  return false;
  }
  *out_timeout_ms = (uint32_t)value;
  return true;
 }

The API reads a zero timeout as "the Hub's default", so a mistyped argument that turned into 0 would silently become 120 s. The program refuses text that is not a whole number of milliseconds and exits with 1:

 approval timeout 'soon' is not a number of milliseconds

2. Load the installed runtime​

  /* A local app always runs the default lookup: the Hub refuses a runtime of
  * another version or flavor than its own. */
  if(!qar_library_load(NULL))
  {
  /* The lookup has printed why: nothing found, a QAROS_RUNTIME_DIR without
  * a runtime, a broken install, or a runtime older than the header. */
  fprintf(stderr, "Could not load QAROS: install or repair it; the line above says why.\n");
  return 2;
  }
  library_init.enable_console_logging = false; /* this app talks to a user */
  if(qar_result_is_error(qar_library_init(&library_init)))
  {
  fprintf(stderr, "The QAROS runtime failed to initialize.\n");
  return 1;
  }

A local app always calls qar_library_load with NULL, never with a path of its own (Load the installed runtime). The Hub refuses a runtime of another version or flavor than its own, such as a private copy shipped with the app, with QAR_STATUS_ONBOARDING_FAILED and the reason in the message.

Console logging is off: this app talks to a user, not a developer. When no runtime loads, the header's loader has already printed where it looked; the program adds one line for the user and exits with 2.

3. Create a runtime​

  /* A local app stores no identity, so it needs no storage folder of its own,
  * and the runtime binaries folder stays empty. */
  QarRuntime* runtime = NULL;
  QarResult created = qar_runtime_create(&runtime_init, &runtime);
  if(qar_result_is_error(created))
  {
  log_result("qar_runtime_create", created);
  return 1;
  }

qar_runtime_init_default() is all a local app needs. With QarRuntimeInit::runtime_binaries_folder_path empty, the runtime uses the folder the library was loaded from, the installed bin folder.

4. Join, and follow the approval​

  init.presentation.display_name = "Local App Onboarding Example";
  local_app.approval_timeout_ms = approval_timeout_ms;
  init.header.next = &local_app.header;
 
  /* Blocks until the Hub answers, which can take minutes on the first run.
  * The onboarding id comes back zero: a local app has nothing to persist. */
  QarSession* session = NULL;
  runtime, &init, example_print_progress, NULL, NULL, &onboarding_id, &session
  );

QarOnboardLocalAppExt, chained as the only mode extension, asks the Hub on this machine to let the process in. The Hub identifies the process from Windows: its path, file hash, signature and user. Your display_name is shown to the user separately, labelled "App says:".

  • Approved before and unchanged: the session comes back at once, with no prompt.
  • Otherwise QAROS shows "Allow local_app_onboarding.exe to join QAROS?" in the Visualizer and as a tray window, in front of every other window. Deny is the default button, and closing the prompt counts as Deny.
  • Allow saves a trust record on the Hub. For an unsigned executable it holds the exact path and file hash, so every rebuild asks again. That is expected during development; sign release builds and users approve them once (The trust record).

qar_runtime_onboard blocks until the Hub answers, which can take minutes. The progress callback shows what it waits for. The library calls it on its own thread while your thread is blocked, and the last call returns before qar_runtime_onboard does. The callback only prints, so it needs no state:

 /** \brief Progress callback that prints each message. The library calls it on
  * its own thread while the blocking call waits; it returns before the call
  * does. */
 static void
 example_print_progress(QarActionSeverity severity, float percent, const char* message, void* state)
 {
  (void)severity;
  (void)percent;
  (void)state;
  if(message != NULL && message[0] != '\0')
  {
  printf("%s\n", message);
  }
 }
 
 /** \brief Exit code for the result of a local-app join, after telling the user
  * what to do: 0 joined, 3 to 7 for the refusals below, 1 for anything else.
  * Each refusal needs a different answer from the user, so each has its own
  * code. */
 static int
 {
  if(qar_result_is_success(result))
  {
  return 0;
  }
  {
  fprintf(
  stderr,
  "Could not start or reach QAROS on this machine. Start QAROS from the Start menu, "
  "then start this app again.\n"
  );
  return 3;
  }
  {
  fprintf(stderr, "QAROS did not allow this app: the approval was denied or revoked.\n");
  return 4;
  }
  {
  fprintf(
  stderr, "Nobody answered the approval prompt in QAROS in time. Start the app again.\n"
  );
  return 5;
  }
  {
  fprintf(
  stderr,
  "The program answering on this machine is not the installed QAROS. Reinstall QAROS.\n"
  );
  return 6;
  }
  {
  fprintf(
  stderr,
  "This app no longer matches what QAROS approved. Revoke it in QAROS and approve it "
  "again.\n"
  );
  return 7;
  }
  log_result("qar_runtime_onboard", result);
  return 1;
 }

The program passes no cancel token, so it can only wait. An application passes a QarCancelToken and cancels it when the user quits.

A local app is stateless. The onboarding id comes back zero, and there is nothing to save: every start calls qar_runtime_onboard again, and the Hub remembers the approval. qar_runtime_rejoin and qar_runtime_forget do not apply.

5. Tell the user, and leave​

  const int exit_code = example_local_app_exit_code(joined);
  if(exit_code == 0)
  {
  printf("Joined QAROS.\n");
  }
  if(session != NULL)
  {
  }
  return exit_code;

Each refusal comes with its own status code, and each needs a different answer from the user. example_local_app_exit_code, shown in step 4, maps them to the exit codes above and prints what to do. None of them is worth retrying silently. A timeout may be offered again; a denial is retried only when the user asks; a Hub that is not authentic is never retried (Status codes and what to tell the user).

The sample programs that use a session join the same way when they are given neither a stored id nor a code (example_join_session in common.h).

Ownership and teardown in C​

  • The session is yours. On success qar_runtime_onboard writes the only handle to it. qar_session_handle_destroy ends it; the Hub keeps the approval, not the session.
  • Reverse order. The session, then the runtime (qar_runtime_handle_destroy), then the library.
  • The init structures are copied. init and local_app live on the stack and are only read during the call.
  • The progress callback runs only during the blocking call. Anything it reaches through its state pointer must live that long.
  • Nothing is written to disk by the app.

Troubleshooting​

What you seeCauseWhat to do
exit 2, Could not load QAROS: install or repair it; the line above says why. after qar-streaming-c.dll is not beside this module, ...no runtime in any step of the lookupinstall QAROS (Installation)
exit 2, the same line after QAROS_RUNTIME_DIR is '...', but no loadable qar-streaming-c.dll is there.the variable names a folder without a loadable runtime; the lookup stops therefix or unset QAROS_RUNTIME_DIR
exit 2, the same line after The installed QAROS runtime in '...' could not be loaded.a broken installationrepair or reinstall QAROS
exit 2, the same line after Failed to load symbol '...'the runtime is older than this repository's headerinstall a QAROS at least as new as the header
exit 3, Could not start or reach QAROS on this machine. Start QAROS from the Start menu, then start this app again.QAROS is installed but its Hub does not answer, and the runtime did not start itstart QAROS from the Start menu, then the app again
exit 4someone clicked Deny, closed the prompt, or an operator revoked the appstart the app again and click Allow
exit 5nobody answered within the timeoutanswer the prompt in the Visualizer or the tray window; pass a longer timeout if users need it
exit 6the program serving the local enrollment is not the installed, signed QAROSdo not retry; reinstall QAROS from the official installer
exit 7the executable was moved, copied, re-signed or changed since its approvalrevoke the old approval in the Visualizer (Approved local apps) and start again
exit 1, QAR_STATUS_ONBOARDING_FAILED with a runtime version or flavor in the messagethe app loaded a runtime other than the installed one, through QAROS_RUNTIME_DIR or a copy beside the executableunset the variable, remove the copy
exit 1, approval timeout '...' is not a number of millisecondsthe argument is not a numberpass milliseconds, or nothing
the prompt comes back after every buildthe executable is unsigned, so its approval is for one exact fileexpected during development

More in Troubleshooting: Joining QAROS as a local app.

Next​


Generated via doxygen2docusaurus 2.2.1 by Doxygen 1.9.8.