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)
{
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];
}
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:
static bool
{
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';
}
static void
{
char text[64] = { 0 };
{
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:
static int
const char* pairing_code,
const char* id_file_path,
const char* display_name,
)
{
{
runtime, &rejoin_init, NULL, NULL, NULL, out_session
);
{
*out_onboarding_id = saved_id;
return 0;
}
))
{
return 1;
}
}
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_code.code = pairing_code;
runtime, &onboard_init, NULL, NULL, NULL, &new_id, out_session
);
log_result("qar_runtime_onboard", onboard_result);
{
{
printf(
"Pairing code rejected or expired — read the current code "
"off the hub screen and retry.\n"
);
}
return 1;
}
*out_onboarding_id = new_id;
return 0;
}
Which is used from main like this:
bool sibling_onboarded = false;
primary_runtime,
pairing_code,
"onboarding_and_rejoin.primary-onboarding-id.txt",
"Primary Tutorial Peer",
&primary_onboarding_id,
&primary_session
) != 0
|| primary_session == NULL)
{
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:
primary_session, &request_init, NULL, NULL, NULL, &primary_invite
);
log_result("qar_session_request_onboarding_invite", invite_result);
{
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
);
runtime_dir, "onboarding_and_rejoin.sibling-state"
);
if(sibling_runtime != NULL)
{
blob, bytes_written, &sibling_invite
);
"qar_onboarding_invite_deserialize", deserialize_result
);
&& sibling_invite != NULL)
{
"Sibling Tutorial Peer";
sibling_mode.invite = sibling_invite;
sibling_runtime,
&sibling_init,
NULL,
NULL,
NULL,
&sibling_onboarding_id,
&sibling_session
);
log_result("qar_runtime_onboard", 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"
);
}
}
}
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:
printf(
"Destroying primary session handle keeps its persisted onboarding "
"state for rejoin.\n"
);
if(forget_on_exit)
{
if(sibling_onboarded)
{
runtime_dir, "onboarding_and_rejoin.sibling-state"
);
if(sibling_runtime != NULL)
{
"qar_runtime_forget(sibling)",
);
}
remove("onboarding_and_rejoin.sibling-onboarding-id.txt");
}
"qar_runtime_forget(primary)",
);
remove("onboarding_and_rejoin.primary-onboarding-id.txt");
}
Generated via doxygen2docusaurus 2.2.1 by Doxygen 1.9.8.