Skip to main content

Onboarding and Rejoin

Overview

Every QarOS session is obtained through onboarding - there is no direct create/join. This tutorial shows the canonical startup flow:

  • Reuse dynamic loading from the previous tutorial
  • Try a silent rejoin with the persisted onboarding id
  • Fall back to onboarding with the pairing code shown by the hub
  • Persist the returned onboarding id for the next run
  • Mint a serialized invite for a sibling API instance
  • Start a second runtime instance and onboard it from that invite
  • Tear down by destroying the session handle (and optionally forget)

Prerequisites

  • Complete the Dynamic Loading Overview tutorial
  • A running QarOS hub on this machine (or reachable on the network)
  • For the first run: the pairing code from the hub's onboarding screen

Build and Run

 cmake --build --preset x64-windows-debug --target onboarding_and_rejoin
 ./build/x64-windows/Debug/onboarding_and_rejoin.exe
 <path-to-qar-streaming-c.dll> [runtime-dir] [pairing-code] [--forget]

Parse Arguments

  if(argc < 2)
  {
  print_usage(argv[0]);
  return 1;
  }
 
  const char* library_path = argv[1];
  const char* runtime_dir = NULL;
  char runtime_dir_buffer[1024] = { 0 };
  const char* pairing_code = NULL;
  bool forget_on_exit = false;
 
  if(argc >= 3)
  {
  runtime_dir = argv[2];
  }
  else if(get_dir_from_path(
  library_path, runtime_dir_buffer, sizeof(runtime_dir_buffer)
  ))
  {
  runtime_dir = runtime_dir_buffer;
  }
 
  for(int i = 3; i < argc; ++i)
  {
  if(strcmp(argv[i], "--forget") == 0)
  {
  forget_on_exit = true;
  }
  else
  {
  pairing_code = argv[i];
  }
  }

Persisting the Onboarding Id

The onboarding id is the only thing an application must store. These shared helpers keep it in a small text file as a UUID string:

 /** \brief Load a persisted onboarding id (UUID text) from a small state file.
  * Returns false when the file does not exist yet (first run). */
 static bool
 load_onboarding_id(const char* path, QarOnboardingId* out_id)
 {
  FILE* file = fopen(path, "r");
  if(!file)
  {
  return false;
  }
  char text[64] = { 0 };
  const bool read_ok = fgets(text, sizeof(text), file) != NULL;
  fclose(file);
  if(!read_ok)
  {
  return false;
  }
  text[strcspn(text, "\r\n")] = '\0';
  QarResult parse_result = qar_uuid_from_string(text, out_id->data);
  return qar_result_is_success(parse_result);
 }
 
 /** \brief Persist an onboarding id (UUID text) so the next run can rejoin. */
 static void
 save_onboarding_id(const char* path, const QarOnboardingId* id)
 {
  char text[64] = { 0 };
  if(qar_result_is_error(qar_uuid_to_string(id->data, text, sizeof(text))))
  {
  printf("Failed to serialize onboarding id\n");
  return;
  }
  FILE* file = fopen(path, "w");
  if(!file)
  {
  printf("Failed to persist onboarding id to '%s'\n", path);
  return;
  }
  fprintf(file, "%s\n", text);
  fclose(file);
 }

Rejoin or Onboard

The canonical flow - rejoin silently when a persisted id exists, otherwise onboard with the code and persist the returned id:

 /** \brief Obtain a session the canonical way: rejoin with the persisted
  * onboarding id when one exists, otherwise onboard with the pairing code
  * currently shown on the hub's onboarding screen.
  *
  * Returns 0 on success; on success *out_session and *out_onboarding_id are
  * set and the id has been persisted to id_file_path for the next run. */
 static int
  QarRuntime* runtime,
  const char* pairing_code, /* may be NULL when a persisted id exists */
  const char* id_file_path,
  const char* display_name,
  QarOnboardingId* out_onboarding_id,
  QarSession** out_session
 )
 {
  presentation.display_name = display_name;
 
  /* 1 Silent rejoin with the persisted identity — the normal path. */
  if(load_onboarding_id(id_file_path, &saved_id))
  {
  rejoin_init.onboarding_id = saved_id;
  rejoin_init.presentation = presentation;
 
  QarResult rejoin_result = qar_runtime_rejoin(
  runtime, &rejoin_init, NULL, NULL, NULL, out_session
  );
  log_result("qar_runtime_rejoin", rejoin_result);
  if(qar_result_is_success(rejoin_result))
  {
  *out_onboarding_id = saved_id;
  return 0;
  }
  ))
  {
  return 1; /* real error (hub unreachable, ...) */
  }
  /* No persisted slot for the id -> fall through to onboarding. */
  }
 
  /* 2 First connect: onboard with the code shown by the hub. */
  if(!pairing_code)
  {
  printf(
  "No persisted onboarding id in '%s'. Pass the pairing code shown "
  "on the hub's onboarding screen.\n",
  id_file_path
  );
  return 1;
  }
 
  onboard_init.presentation = presentation;
  onboard_code.code = pairing_code;
  onboard_init.header.next = &onboard_code.header;
 
  QarResult onboard_result = qar_runtime_onboard(
  runtime, &onboard_init, NULL, NULL, NULL, &new_id, out_session
  );
  log_result("qar_runtime_onboard", onboard_result);
  if(qar_result_is_error(onboard_result))
  {
  {
  printf(
  "Pairing code rejected or expired — read the current code "
  "off the hub screen and retry.\n"
  );
  }
  return 1;
  }
 
  /* 3 Persist the id — it is the ticket for every later rejoin/forget. */
  save_onboarding_id(id_file_path, &new_id);
  *out_onboarding_id = new_id;
  return 0;
 }

Which is used from main like this:

  QarOnboardingId primary_onboarding_id = qar_onboarding_id_default();
  QarOnboardingId sibling_onboarding_id = qar_onboarding_id_default();
  bool sibling_onboarded = false;
  QarSession* primary_session = NULL;
  primary_runtime,
  pairing_code,
  "onboarding_and_rejoin.primary-onboarding-id.txt",
  "Primary Tutorial Peer",
  &primary_onboarding_id,
  &primary_session
  ) != 0
  || primary_session == NULL)
  {
  qar_runtime_destroy(primary_runtime);
  return 5;
  }
 
  char primary_id_text[64] = { 0 };
  primary_onboarding_id.data, primary_id_text, sizeof(primary_id_text)
  )))
  {
  printf(
  "Primary session obtained with onboarding id %s\n", primary_id_text
  );
  }

Share an Invite with a Second API Instance

A second API instance in the same application (another process, or a C# binding next to native code) must not pair with a code again. The primary onboarded instance requests an invite, serializes it, and the sibling instance consumes it with qar_runtime_onboard:

  QarOnboardingInvite* primary_invite = NULL;
  primary_session, &request_init, NULL, NULL, NULL, &primary_invite
  );
  log_result("qar_session_request_onboarding_invite", invite_result);
  if(qar_result_is_success(invite_result) && primary_invite != NULL)
  {
  size_t serialized_size = 0;
  primary_invite, &serialized_size
  ));
 
  uint8_t* blob = (uint8_t*)calloc(1, serialized_size);
  if(blob != NULL)
  {
  size_t bytes_written = 0;
  primary_invite, blob, serialized_size, &bytes_written
  ));
 
  int64_t expires_unix = 0;
  primary_invite, &expires_unix
  ));
  printf(
  "Minted sibling invite (%zu bytes, expires at unix %lld).\n",
  bytes_written,
  (long long)expires_unix
  );
 
  QarRuntime* sibling_runtime = create_runtime_or_null(
  runtime_dir, "onboarding_and_rejoin.sibling-state"
  );
  if(sibling_runtime != NULL)
  {
  QarOnboardingInvite* sibling_invite = NULL;
  QarResult deserialize_result =
  blob, bytes_written, &sibling_invite
  );
  "qar_onboarding_invite_deserialize", deserialize_result
  );
  if(qar_result_is_success(deserialize_result)
  && sibling_invite != NULL)
  {
  QarOnboardInviteExt sibling_mode =
  sibling_init.presentation.display_name =
  "Sibling Tutorial Peer";
  sibling_mode.invite = sibling_invite;
  sibling_init.header.next = &sibling_mode.header;
 
  QarOnboardingId sibling_onboarding_id =
  QarSession* sibling_session = NULL;
  QarResult sibling_result = qar_runtime_onboard(
  sibling_runtime,
  &sibling_init,
  NULL,
  NULL,
  NULL,
  &sibling_onboarding_id,
  &sibling_session
  );
  log_result("qar_runtime_onboard", sibling_result);
  if(qar_result_is_success(sibling_result)
  && sibling_session != NULL)
  {
  char sibling_id_text[64] = { 0 };
  sibling_onboarding_id.data,
  sibling_id_text,
  sizeof(sibling_id_text)
  )))
  {
  printf(
  "Sibling runtime onboarded with onboarding id "
  "%s\n",
  sibling_id_text
  );
  }
 
  "onboarding_and_rejoin.sibling-onboarding-id.txt",
  &sibling_onboarding_id
  );
  sibling_onboarded = true;
  printf(
  "Destroying sibling session handle keeps its "
  "persisted onboarding state for rejoin.\n"
  );
  qar_session_handle_destroy(sibling_session);
  }
 
  }
 
  qar_runtime_destroy(sibling_runtime);
  }
 
  free(blob);
  }
 
  }

Destroy and Forget

Destroying the session handle keeps the persisted identity so the next run can rejoin. Forget erases it - only do that for an explicit "un-pair this device" action:

  /* Destroying the handle leaves the session and keeps persisted identity,
  * so the next run can rejoin silently. */
  printf(
  "Destroying primary session handle keeps its persisted onboarding "
  "state for rejoin.\n"
  );
  qar_session_handle_destroy(primary_session);
 
  if(forget_on_exit)
  {
  /* Full un-pair: erase the identity slot. The next run needs a fresh
  * pairing code. Must happen after the session was left. */
  if(sibling_onboarded)
  {
  QarRuntime* sibling_runtime = create_runtime_or_null(
  runtime_dir, "onboarding_and_rejoin.sibling-state"
  );
  if(sibling_runtime != NULL)
  {
  QarForgetInit sibling_forget_init = qar_forget_init_default();
  sibling_forget_init.onboarding_id = sibling_onboarding_id;
  "qar_runtime_forget(sibling)",
  qar_runtime_forget(sibling_runtime, &sibling_forget_init)
  );
  qar_runtime_destroy(sibling_runtime);
  }
  remove("onboarding_and_rejoin.sibling-onboarding-id.txt");
  }
 
  forget_init.onboarding_id = primary_onboarding_id;
  "qar_runtime_forget(primary)",
  qar_runtime_forget(primary_runtime, &forget_init)
  );
  remove("onboarding_and_rejoin.primary-onboarding-id.txt");
  }

Generated via doxygen2docusaurus 2.2.1 by Doxygen 1.9.8.