Phone compatibility internals

CNA snapshot 009d40f5  ·  Development › Module internals  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. The 18 tests named here were located and their CTest route read; none was executed. The native Windows notification path, shutdown against a hostile or stalled peer and SIGPIPE exposure remain unverified; the Winsock branches have no test on any host.

modules/phone/ supplies the small Microsoft::Phone surface that Windows Phone 7 XNA titles were written against. PhoneApplicationService re-expresses Game's lifecycle events as the phone's Launching, Activated, Deactivated and Closing, and HttpNotificationChannel with two push-message classes implements a loopback-only HTTP notification path. Nothing installs either adapter automatically, and inside the repository nothing but the test executables links the module. This page is for maintainers porting a phone title, changing Game's activation or exit semantics, or touching the socket code; the user-level context is the Microsoft.Devices row of XNA compatibility, namespace by namespace. Treat the module as a compatibility boundary: it is not a platform backend, a general network stack, an operating-system push service or persistent storage.

Where it sits in the build and execution graph

modules/phone/CMakeLists.txt globs src/*.cpp (three translation units at this snapshot) into cna_add_module(cna_phone Phone …), the helper in modules/CMakeLists.txt that makes a STATIC library cna_phone with the alias CNA::Phone. It links cna_runtime and cna_core_headers publicly (the lifecycle it reports is the one Game already receives) and the Sharp Runtime components Core.Base, Collections.Core and Uri. CNA's own module table in docs/physical-modules.md lists only the first two components; the CMake file is the authority.

modules/CMakeLists.txt adds the directory unconditionally (no CMake option gates it, so CNA's own build compiles it in every configuration) but leaves cna_phone out of the _cna_runtime_parts list that defines the CNA umbrella. Everything else follows from that one omission:

  • Games opt in by name. Linking CNA gives a game neither the phone symbols nor a listener thread; a port names CNA::Phone itself (the module layout table says “linked when a game names it”).
  • Not inside libcna.so. With CNA_SHARED_LIBRARY the shared runtime is assembled from the same parts list, so the phone module stays a separate static archive.
  • Only tests link it. The focused CnaPhoneTests gets it from CNA_TEST_GROUP_DEPENDENCY_phone (cna_phone cna_runtime, because the tests construct a Game), and the aggregate CnaTests names it explicitly in cmake/UnitTests.cmake, since it consumes every group's objects. No example, tool or other module links it.
  • No C routes. CNA's C coverage inventory records Microsoft::Phone as out of runtime C scope by owner decision (CBIND-117, in generate_coverage_inventory.py), which is what the C API page's measured coverage reports.
  • The port-facing calls are extensions. AttachEXT, DetachEXT and DispatchPendingNotificationsEXT carry the CNAEXT marker from CNAHelper.hpp. Under CNA_STRICT_XNA_API that marker expands to [[deprecated]], so a strict build warns on exactly the three calls a phone port cannot avoid.
Game (modules/runtime)                                  PhoneApplicationService (borrowed Game*)
  BeforeLoop, FocusGained, DidEnterForeground
      -> Game::Activated   --------------------------->  ignored until a Deactivated, then
                                                           phone Activated(preserved = true)
  FocusLost, WillEnterBackground
      -> Game::Deactivated --------------------------->  phone Deactivated
  loop ends (Exit(), Terminating -> Exit())
      -> Game::Exiting     --------------------------->  phone Closing
  explicit AttachEXT(game) --------------------------->  phone Launching, immediately

Sender (caller's thread)       channel worker thread          thread that calls dispatch
  SendAsync(uri)                 accept (one at a time)
    connect, POST, recv  ---->   ReadRequestBody
                                 reply 200 / 400, close
                                 pending_.push_back [mutex_] -> DispatchPendingNotificationsEXT
                                                                 swap under mutex_, then raise
                                                                 HttpNotificationReceived

A source directory does not imply runtime integration: this is the clearest case in the tree. Compare the Devices module, which the CNA umbrella does include (Devices and sensor lifetime), and the full list in the module index.

Lifecycle translation and ownership

PhoneApplicationService.hpp declares the four events, the State dictionary and the two extension calls; PhoneApplicationService.cpp is about ninety lines. getCurrentProperty returns a function-local static instance, but the constructor is public, so tests and applications can also build their own. Copying is deleted, and because a copy constructor is declared the class is not movable either: the handlers it installs capture this, so the object must stay where it was created.

AttachEXT(Game&) first calls DetachEXT, then stores a borrowed Game*, resets its “has been deactivated” flag, subscribes three lambdas to the game's Deactivated, Activated and Exiting events (keeping the three tokens), and finally raises Launching synchronously on the calling thread. Every phone event is raised with a null sender. The mapping, including where Game.cpp raises each Game event, is:

Game eventRaised by the runtime whenPhone event
ActivatedsetIsActiveProperty(true): BeforeLoop at the start of Run, a FocusGained window event, or an AppLifecycleKind::DidEnterForeground event. Raised only on a change of IsActive.Ignored until the first Deactivated since the attach; after that, Activated with IsApplicationInstancePreserved = true on every activation.
DeactivatedsetIsActiveProperty(false): FocusLost, or WillEnterBackground (which also parks the loop on mobile targets).Deactivated, every time; sets the flag.
ExitingOnExiting after the native loop ends (or in the Emscripten frame callback when RunApplication clears); AppLifecycleKind::Terminating reaches it through Exit().Closing.
(none)The application calls AttachEXT.Launching, once per attach.

Four consequences are easy to miss:

  • On desktop a focus loss is a phone deactivation. Switching to another window and back produces Deactivated then Activated(preserved): Game reports a window focus change and a mobile backgrounding through the same property, and the adapter cannot tell them apart. A title that saves from Deactivated (as the header says it should) therefore saves on every task switch.
  • Activated always says the instance was preserved. The handler runs in the process that went away, so it passes true; the default-constructed ActivatedEventArgs (preserved = false) is never produced by the adapter. A CNA process that was killed comes back as a new process and a new Launching.
  • Launching is per attach, not per process. Re-attaching, or attaching a second game, raises it again. A host driven by RunOneFrame never calls BeforeLoop, so its first activation comes from a focus event; the flag makes the adapter ignore that one as well.
  • Closing comes early in teardown. Exiting fires before Dispose and UnloadContent (see the Game lifecycle diagram), so content is still loaded in a Closing handler.

DetachEXT returns at once when nothing is attached. Otherwise it removes the three tokens from the attached game's events, then clears the pointer, the tokens and the flag. The destructor calls it. The service may therefore die before the game (the test DestroyingTheServiceUnsubscribesFromTheGame pins that order), but the game must not die first, because DetachEXT dereferences the stored pointer.

⚠

The process-wide Current is the risky case. The obvious port, PhoneApplicationService::getCurrentProperty().AttachEXT(game) with the Game on main's stack, leaves the function-static service attached to a game that is destroyed when main returns. The static's destructor then runs DetachEXT against a dead object during static destruction. Nothing in Game or the runtime detaches it. Call DetachEXT after Run returns, or from the game's destructor, before the Game goes away. This is a source-level finding: every test uses a local service, and none covers this order.

getStateProperty returns a member Dictionary<std::string, std::shared_ptr<System::Object>>. It is plain memory: it is not serialized, it does not touch the storage module, and DetachEXT does not clear it. It “survives” a deactivate and reactivate only because the process never went away. A title that must survive process loss writes to storage from Deactivated, as the header advises; see Storage internals and the storage root chain.

Threading: the phone events run synchronously on whichever thread raises the Game event, normally the thread inside Game::Run. Launching runs on the thread that calls AttachEXT. The service has no lock, so attaching or detaching while another thread raises Game events is unsynchronized. The thread and callback map and ownership map give the runtime-wide picture; Runtime lifecycle and Runtime module internals explain where the Game events come from.

Notification path, threads and limits

HttpNotificationChannel.hpp states the design: on Windows Phone the channel URI pointed at Microsoft's push relay, which is retired, so here the channel is the endpoint. What goes with the relay is the ability to reach a device that is not directly addressable and to deliver to an application that is not running. HttpNotificationChannel.cpp implements Open as follows:

  1. If listening_ (an std::atomic<bool>) is already set, return. The check is a plain load, not a compare-exchange, so two concurrent Open calls are not guarded against each other.
  2. Create an IPv4 SOCK_STREAM socket, set SO_REUSEADDR, bind INADDR_LOOPBACK on port 0 (the system picks the port), and listen with a backlog of 4. A failure to create the socket, or a failed bind or listen, logs a Logger::Warn and returns with no URI. A failed getsockname returns silently.
  3. Store the socket and build http://127.0.0.1:<port>/<channelName>/. The name is spliced in unescaped; how System::Uri treats a name that is not a valid path segment is Sharp Runtime behaviour that no phone test covers. Then set listening_.
  4. Under the registry mutex, assign Registry()[channelName] = this. This is an assignment, so a duplicate name silently replaces the earlier entry.
  5. Start one std::thread running Listen.
  6. Raise ChannelUriUpdated on the calling thread. The worker is already running, so a notification can be queued before the handler learns the URI. That is harmless, because it waits in the queue.

Listen loops while listening_ is set. It accepts one connection at a time, reads it with ReadRequestBody, sends a fixed response, closes the connection, and queues the body under mutex_ if it is non-empty. The worker never raises an event. The parser only looks for the header terminator and a content-length: substring in the lower-cased header block (the first occurrence wins). It never inspects the method or the path, so any request to the port reaches the channel, whatever its path.

What the peer sendsResponseQueued
Headers, Content-Length: N, then N bytes200Exactly N bytes; anything after them is dropped
No Content-Length (including a chunked body)200Nothing: the length is taken as 0 and empty bodies are never queued
Content-Length: N, then the peer closes after fewer bytes200The truncated body
Connection closed or failed before \r\n\r\n, or a header block over 1 MiB400Nothing
💡

The parser's comment promises more than the code does. The comment above ReadRequestBody says unsupported encodings are “refused visibly” rather than “delivered truncated”. In the source, a chunked push is acknowledged with 200 and dropped, and a short body is acknowledged with 200 and queued truncated. Only an unreadable header block gets a 400. The body size is not bounded either: the worker keeps reading until Content-Length bytes arrive or the peer stops. A 200 means the socket received the bytes, never that a handler ran.

The listener has no receive timeout: no SO_RCVTIMEO, no poll. A peer that connects and sends nothing parks the only worker in recv, later connections wait in the backlog of 4, and Close cannot wake it, because Close shuts down only the listening socket. The source therefore suggests that Close, and the destructor, can wait on such a peer indefinitely. Shutdown behaviour against a hostile or stalled peer remains unverified: no test exercises it. Two smaller hazards are also visible in the source. A persistent accept error while listening loops again at once, with no backoff. And responses are sent with flags 0, without the MSG_NOSIGNAL/SO_NOSIGPIPE handling that the Inspector's socket layer uses (InternalSocket.cpp), so whether a peer that resets the connection can raise SIGPIPE depends on the process's signal disposition. That has not been exercised either.

The trust boundary is the loopback bind and nothing else. There is no authentication and no channel secret. A remote service cannot reach the advertised URI, but any local process that finds the port can post to it. Offline delivery, retries and a remote relay do not exist.

DispatchPendingNotificationsEXT swaps pending_ into a local vector under mutex_, releases the lock, and raises HttpNotificationReceived once per body, in arrival order, on the caller's thread. No production code calls it; an application chooses its dispatch point, typically once per frame on the game thread. Calling it from a worker moves the handlers to that worker, because the API does not enforce thread affinity. Sharp Runtime's System::EventHandler::Raise iterates a snapshot of the handlers and does not catch, so a handler that throws propagates out of the dispatch call, and the rest of that swapped-out batch is lost.

Close returns early if the channel is not listening. Otherwise it clears listening_, calls shutdown (SHUT_RDWR, or SD_BOTH on Windows) and then closes the listening socket; the code comments that closing alone can leave the worker parked in accept. It then joins the worker, erases the registry entry only if that entry still points at this channel, and resets the URI. The destructor calls Close. Some state deliberately survives Close: pending_ is not cleared, so bodies received before the close can still be dispatched (and a reopened channel still holds them), and shellToastBound_ keeps its value. A reopened channel gets a new port, and so a new URI and a new ChannelUriUpdated; the sending service has to be told.

Find looks the name up under the registry mutex and returns the raw pointer after releasing it. That pointer is not a lease: a concurrent Close or destruction can invalidate it. Duplicate names are not rejected. With two open channels of one name, the second Open overwrites the entry. The first channel's Close leaves that entry alone (the identity check), but the second channel's Close erases the name while the first is still open, so Find then returns null for a live channel.

Concurrency: only listening_ (atomic) and pending_ (under mutex_) are synchronized. channelUri_, shellToastBound_, the thread object and listenSocket_, a plain int that the worker reads and Close resets, are not. Keep Open, Close, the property getters and BindToShellToast on one owning thread, and treat only the queue as a cross-thread structure. The constructor's serviceName is stored and never read.

Sender semantics and failure boundaries

PushNotificationSender.hpp declares MessageSendPriority (Low, Normal, High) and two classes: RawPushNotificationMessage, which sends its RawData bytes, and ToastPushNotificationMessage, which sends Title and SubTitle. No class is called PushNotificationSender; that is only the file name. In PushNotificationSender.cpp both SendAsync methods call one internal Post:

  1. Take clientUri.ToString() and split it by string search into host[:port] and a path. It returns false only if // is missing. The port defaults to 80 and the scheme is never checked, so an https URI would be sent as plain HTTP to port 80.
  2. Call getaddrinfo with AF_INET (IPv4 only), then create a socket and do a blocking connect. A lookup, socket or connect failure returns false.
  3. Build the request and write it with one send call. The length is narrowed to int for the Windows signature, and a short write is not retried: the method returns false.
  4. Do one blocking recv of up to 512 bytes, discard it, close the socket, and return whether the whole request was sent.
Request elementRaw messageToast message
X-NotificationClassHigh 3, Normal 13, Low 23High 2, Normal 12, Low 22
X-WindowsPhone-Targetabsenttoast
Content-Typetext/xml (even for arbitrary bytes)text/xml
BodyRawData verbatim<wp:Notification xmlns:wp="WPNotification"><wp:Toast><wp:Text1>Title</wp:Text1><wp:Text2>SubTitle…, with an XML declaration
AlwaysPOST <path> HTTP/1.1, Host, Content-Length, Connection: close

Three semantic points matter to anyone changing reliability or security. First, “Async” is synchronous: name resolution, connect, send and the response wait all block the caller, without timeouts, so a send from the game thread stalls that frame. Second, the header documents the return value as “true when the receiver accepted it”, but the code returns true whenever the bytes were written; a 400 still yields true, and nothing parses the status line. Third, the toast body concatenates Title and SubTitle without XML escaping, so an & or < in a title produces malformed XML. The channel does not parse the XML and delivers the bytes anyway. BindToShellToast on the receiving channel only records a flag; nothing is ever drawn over another application. Both halves of this local protocol have to change together; do not promise internet push delivery or non-blocking sends on top of them.

The Windows path

Both source files carry Winsock branches (closesocket, SD_BOTH). CNA's native-Windows validation plan (plan_win32_native_validation.md, rows F11 and F16) records two compile fixes in these files. F11 undefines the ERROR macro that <windows.h> defines, which collided with CNA::LogLevel::ERROR. F16 replaced a use of ssize_t, which MSVC does not define. What the source does not show:

  • No WSAStartup call exists in the module (the only one in CNA's tree is in the Inspector's socket layer). Whether Winsock is initialized when Open or SendAsync runs therefore depends on what else the process has done, and nothing establishes it.
  • The checks listener < 0 and connection < 0 compare an unsigned SOCKET, so they never detect INVALID_SOCKET. Failure still surfaces, later, through the bind, listen or connect paths.
  • HttpNotificationChannelTests.cpp, which also holds the PushNotificationSenderTest cases, is removed from the test sources under WIN32 because its test-side client uses POSIX socket headers. The exclusion removes the tests, not the feature.

The native Windows notification path therefore has no test and remains unverified; only the lifecycle tests are in a Windows CnaTests.

What the tests pin down

The module has 18 GoogleTest definitions in two files. They were located and read at 009d40f5; none was executed for this page.

SuiteCasesWhat they assert
PhoneApplicationServiceTest (9, PhoneApplicationServiceTests.cpp)AttachingReportsAFreshStart, TheFirstActivationOfARunIsNotAReturnFromTheBackground, GoingAwayAndComingBackIsDeactivatedThenActivated, ExitingTheGameClosesTheApplication, DetachingStopsTheServiceFollowingTheGame, AttachingASecondGameLeavesTheFirstOneBehind, DestroyingTheServiceUnsubscribesFromTheGame, StateKeepsWhatTheApplicationPutsThere, CurrentIsOneServiceForTheWholeProcessThe recorded event sequence for attach, first activation, deactivate/activate, exit, detach, re-attach and service destruction; State storage; Current identity. The tests construct a Game and call Raise on its events directly; no platform, window or OS lifecycle is involved.
HttpNotificationChannelTest (6)AClosedChannelHasNoUri, OpeningReportsAnAddressAServiceCanPushTo, WhatAServicePostsArrivesAsANotification, NothingIsRaisedUntilTheGameAsksForIt, AnOpenChannelCanBeFoundByName, ShellToastBindingIsRecordedThe URI shape and reset, delivery of a well-formed POST from a bare POSIX client, queue-until-dispatch (checked by a 100 ms sleep), single-channel Find, and the toast flag.
PushNotificationSenderTest (3)WhatTheSenderPostsIsWhatTheChannelReceives, AToastCarriesItsTitleToTheReceiver, SendingToAnAddressNobodyIsListeningOnFailsSender against channel for raw and toast messages; a refused connection on loopback port 1 returns false.

Registration: cmake/UnitTests.cmake puts both files in the phone group, whose object library feeds the aggregate CnaTests. CTest sees the cases only through gtest_discover_tests(CnaTests … DISCOVERY_MODE PRE_TEST). CnaPhoneTests is an EXCLUDE_FROM_ALL iteration target, which the file states is “not additional CTest registrations”, and unlike core and math it has no build preset. The Windows exclusion is described above.

No test covers the following: malformed, chunked, truncated or oversized requests; the 400 path; duplicate channel names; Close with bodies still pending or with a stalled peer; concurrent Open/Close; a handler that throws during dispatch; the static Current outliving its game; or a real focus or mobile lifecycle driving the adapter. See Test architecture and change recipes for where a new case belongs.

Worked maintainer route: notification never reaches a handler

  1. Is the module linked and the channel open? Check that the executable links CNA::Phone (CNA alone does not bring it in) and that getChannelUriProperty() is non-null after Open. If it is null, look for the HttpNotificationChannel: warnings from socket, bind or listen failure (a getsockname failure is silent) before touching Game or renderer code.
  2. Does the sender use the reported URI? The port changes on every Open, so a URI cached across a Close/Open is stale. A true from SendAsync proves only that the bytes were written: it does not prove HTTP acceptance, and it does not prove that a handler ran.
  3. Did the worker queue it? Break at the pending_.push_back in Listen. If it is never reached, check what the client sent against the response table above: no Content-Length and an empty body both end here silently.
  4. Does anything dispatch? Break in DispatchPendingNotificationsEXT. If bytes are pending but no handler runs, find the application's dispatch call; the runtime installs none. A handler that throws also discards the rest of its batch.
  5. Wrong thread? If a callback runs on an unsafe thread, trace who calls dispatch rather than moving work onto the listener thread. Keep the mutex-protected queue and the rule that events are raised outside the lock.
  6. Pin it. Add a focused case reproducing the exact boundary (open, POST, queue, dispatch or close), then run the notification suites on a POSIX host and the lifecycle suite for any Game event change. A real application shutdown and a Windows build and host still need separate checks, because the listener tests do not compile there.

A focused iteration loop, as the CMake files define it (not executed for this page):

cmake --preset unit                                   # STUB renderer, CNA_BUILD_TESTS=ON
cmake --build cmake-build-unit --target CnaPhoneTests
./cmake-build-unit/CnaPhoneTests --gtest_filter='PhoneApplicationServiceTest.*'
./cmake-build-unit/CnaPhoneTests --gtest_filter='HttpNotificationChannelTest.*:PushNotificationSenderTest.*'

For lifetime bugs around Exiting and destruction, I need to debug shutdown and lifetime behavior and Ownership and shutdown cover the Game side. For a new regression case, see I need to add a regression test; the Maintainer Handbook lists the other routes.

Source-reading order and evidence

  1. phone/CMakeLists.txt, the umbrella list in modules/CMakeLists.txt and the phone lines of UnitTests.cmake: establish the link opt-in, the two test consumers and the WIN32 exclusion before assuming any coverage.
  2. PhoneApplicationService.hpp, then PhoneApplicationService.cpp: trace the borrowed Game*, token removal, the first-activation flag and the in-process State. Then read setIsActiveProperty, BeforeLoop and the window-event and lifecycle cases of PollEvents in Game.cpp to see which platform events become which phone events.
  3. HttpNotificationChannel.hpp, then HttpNotificationChannel.cpp: draw the worker, queue, dispatch and Close/join order, and check what ReadRequestBody actually accepts.
  4. PushNotificationSender.hpp and PushNotificationSender.cpp: compare the synchronous send and its return value with the channel's acknowledgement and handler dispatch.
  5. PhoneApplicationServiceTests.cpp and HttpNotificationChannelTests.cpp: separate the deterministic adapter tests from the missing native OS, network and Windows coverage.

Evidence level: everything above was checked by reading the TARGET sources at 009d40f5; nothing was built or executed. The Game event sources were read in the runtime module, and the two Sharp Runtime behaviours mentioned (EventHandler::Raise, System::Uri) belong to that sibling repository rather than to CNA.

The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.