[camera] Improve the stability of WebRTC session establishment (#39118)

* [camera] Improve the stability of WebRTC session establishment

* Fix memory leak
diff --git a/examples/camera-controller/args.gni b/examples/camera-controller/args.gni
index 5f38802..fc40544 100644
--- a/examples/camera-controller/args.gni
+++ b/examples/camera-controller/args.gni
@@ -34,3 +34,4 @@
 chip_tlv_validate_char_string_on_write = true
 
 chip_build_controller_dynamic_server = true
+chip_device_config_enable_wifipaf = false
diff --git a/examples/camera-controller/commands/common/CHIPCommand.h b/examples/camera-controller/commands/common/CHIPCommand.h
index b5f3c70..c9e2aab 100644
--- a/examples/camera-controller/commands/common/CHIPCommand.h
+++ b/examples/camera-controller/commands/common/CHIPCommand.h
@@ -99,10 +99,6 @@
         AddArgument("commissioner-vendor-id", 0, UINT16_MAX, &mCommissionerVendorId,
                     "The vendor id to use for camera-controller. If not provided, chip::VendorId::TestVendor1 (65521, 0xFFF1) will "
                     "be used.");
-        AddArgument("start-websocket-server", 0, 1, &mStartWebSocketServer,
-                    "Start the built‑in WebSocket server that exposes the interactive‑command API. "
-                    "If not provided or 0 (\"false\"), the WebSocket server is disabled. "
-                    "If 1 (\"true\"), the WebSocket server is started and listens on the default port.");
     }
 
     /////////// Command Interface /////////
@@ -164,7 +160,6 @@
     PersistentStorage mCommissionerStorage;
 #endif // CONFIG_USE_LOCAL_STORAGE
     chip::Optional<char *> mLogFilePath;
-    chip::Optional<bool> mStartWebSocketServer;
     chip::PersistentStorageOperationalKeystore mOperationalKeystore;
     chip::Credentials::PersistentStorageOpCertStore mOpCertStore;
     static chip::Crypto::RawKeySessionKeystore sSessionKeystore;
diff --git a/examples/camera-controller/commands/interactive/InteractiveCommands.cpp b/examples/camera-controller/commands/interactive/InteractiveCommands.cpp
index 6cfa5d9..1bf29a6 100644
--- a/examples/camera-controller/commands/interactive/InteractiveCommands.cpp
+++ b/examples/camera-controller/commands/interactive/InteractiveCommands.cpp
@@ -44,22 +44,47 @@
 // File pointer for the log file
 FILE * sLogFile = nullptr;
 
+// Global flag to signal shutdown
+std::atomic<bool> sShutdownRequested(false);
+
 std::queue<std::string> sCommandQueue;
 std::mutex sQueueMutex;
 std::condition_variable sQueueCondition;
 
 void ReadCommandThread()
 {
-    char * command;
-    while (true)
+    for (;;)
     {
-        command = readline(kInteractiveModePrompt);
-        if (command != nullptr && *command)
+        // `readline()` allocates with `malloc()`.  Wrap it in a smart
+        // pointer so we cannot leak it on every early–return/throw path.
+        std::unique_ptr<char, decltype(&std::free)> rawLine{ readline(kInteractiveModePrompt), &std::free };
+
+        if (!rawLine) // EOF or fatal error → shut down cleanly
         {
-            std::unique_lock<std::mutex> lock(sQueueMutex);
-            sCommandQueue.push(command);
-            free(command);
+            std::lock_guard<std::mutex> lk{ sQueueMutex };
+            sCommandQueue.emplace(kInteractiveModeStopCommand);
             sQueueCondition.notify_one();
+            break;
+        }
+
+        // Ignore empty lines produced by just hitting <Enter>.
+        if (*rawLine == '\0')
+            continue;
+
+        // Copy into an owning `std::string` before the pointer vanishes.
+        std::string line{ rawLine.get() };
+
+        {
+            std::lock_guard<std::mutex> lk{ sQueueMutex };
+            sCommandQueue.push(line);
+        }
+        sQueueCondition.notify_one();
+
+        // Bail out when the user asks to quit.
+        if (line == kInteractiveModeStopCommand)
+        {
+            ChipLogProgress(NotSpecified, "ReadCommandThread exit on quit");
+            break;
         }
     }
 }
@@ -114,7 +139,14 @@
 std::string InteractiveStartCommand::GetCommand() const
 {
     std::unique_lock<std::mutex> lock(sQueueMutex);
-    sQueueCondition.wait(lock, [&] { return !sCommandQueue.empty(); });
+
+    // Wait until queue is not empty OR shutdown is requested
+    sQueueCondition.wait(lock, [&] { return !sCommandQueue.empty() || sShutdownRequested.load(); });
+
+    if (sShutdownRequested.load())
+    {
+        return {}; // empty string signals caller to exit
+    }
 
     std::string command = sCommandQueue.front();
     sCommandQueue.pop();
@@ -161,25 +193,6 @@
         Logging::SetLogRedirectCallback(LoggingCallback);
     }
 
-    bool startWebSocketServer = mStartWebSocketServer.ValueOr(false);
-    if (startWebSocketServer)
-    {
-        InteractiveServerCommand * command =
-            static_cast<InteractiveServerCommand *>(CommandMgr().GetCommandByName("interactive", "server"));
-        if (command == nullptr)
-        {
-            ChipLogError(NotSpecified, "Interactive server command not found.");
-            return CHIP_ERROR_INTERNAL;
-        }
-
-        CHIP_ERROR err = command->RunCommand();
-        if (err != CHIP_NO_ERROR)
-        {
-            ChipLogError(NotSpecified, "Failed to run interactive server command: %" CHIP_ERROR_FORMAT, err.Format());
-            return err;
-        }
-    }
-
     std::thread readCommands(ReadCommandThread);
     readCommands.detach();
 
@@ -187,7 +200,12 @@
     while (true)
     {
         std::string command = GetCommand();
-        if (!command.empty() && !ParseCommand(command, &status))
+        if (command.empty())
+        {
+            break;
+        }
+
+        if (!ParseCommand(command, &status))
         {
             break;
         }
@@ -438,6 +456,8 @@
 
     gInteractiveServerResult.Reset();
     SetCommandExitStatus(CHIP_NO_ERROR);
+    CloseLogFile();
+
     return CHIP_NO_ERROR;
 }
 
@@ -466,6 +486,11 @@
     return shouldStop;
 }
 
+void InteractiveServerCommand::StopCommand()
+{
+    mWebSocketServer.Stop();
+}
+
 CHIP_ERROR InteractiveServerCommand::LogJSON(const char * json)
 {
     gInteractiveServerResult.MaybeAddResult(json);
diff --git a/examples/camera-controller/commands/interactive/InteractiveCommands.h b/examples/camera-controller/commands/interactive/InteractiveCommands.h
index 418324c..cacf2ef 100644
--- a/examples/camera-controller/commands/interactive/InteractiveCommands.h
+++ b/examples/camera-controller/commands/interactive/InteractiveCommands.h
@@ -87,6 +87,8 @@
     /////////// RemoteDataModelLoggerDelegate interface /////////
     CHIP_ERROR LogJSON(const char * json) override;
 
+    void StopCommand();
+
 private:
     WebSocketServer mWebSocketServer;
     chip::Optional<uint16_t> mPort;
diff --git a/examples/camera-controller/main.cpp b/examples/camera-controller/main.cpp
index 0186d11..cf622da 100644
--- a/examples/camera-controller/main.cpp
+++ b/examples/camera-controller/main.cpp
@@ -39,8 +39,8 @@
     // Convert command line arguments to a vector of strings for easier manipulation
     std::vector<std::string> args(argv, argv + argc);
 
-    // Check if "interactive" and "start" are not in the arguments
-    if (args.size() < 3 || args[1] != "interactive" || args[2] != "start")
+    // Check if "interactive" is not in the arguments
+    if (args.size() < 3 || args[1] != "interactive")
     {
         // Insert "interactive" and "start" after the executable name
         args.insert(args.begin() + 1, "interactive");
diff --git a/examples/common/websocket-server/WebSocketServer.cpp b/examples/common/websocket-server/WebSocketServer.cpp
index 402754d..60c366a 100644
--- a/examples/common/websocket-server/WebSocketServer.cpp
+++ b/examples/common/websocket-server/WebSocketServer.cpp
@@ -182,26 +182,44 @@
     };
     info.retry_and_idle_policy = &retry;
 
-    auto context = lws_create_context(&info);
-    VerifyOrReturnError(nullptr != context, CHIP_ERROR_INTERNAL);
+    mContext = lws_create_context(&info);
+    VerifyOrReturnError(mContext != nullptr, CHIP_ERROR_INTERNAL);
 
     mRunning  = true;
     mDelegate = delegate;
 
     while (mRunning)
     {
-        lws_service(context, -1);
+        lws_service(mContext, -1);
 
         std::lock_guard<std::mutex> lock(gMutex);
-        if (gMessageQueue.size())
+        if (!gMessageQueue.empty())
         {
             lws_callback_on_writable(gWebSocketInstance);
         }
     }
-    lws_context_destroy(context);
+
+    lws_context_destroy(mContext);
+    mContext = nullptr;
     return CHIP_NO_ERROR;
 }
 
+void WebSocketServer::Stop()
+{
+    if (!mRunning)
+    {
+        return;
+    }
+
+    mRunning = false;
+
+    // Wake the poll/sleep inside lws_service()
+    if (mContext != nullptr)
+    {
+        lws_cancel_service(mContext);
+    }
+}
+
 bool WebSocketServer::OnWebSocketMessageReceived(char * msg)
 {
     auto shouldContinue = mDelegate->OnWebSocketMessageReceived(msg);
diff --git a/examples/common/websocket-server/WebSocketServer.h b/examples/common/websocket-server/WebSocketServer.h
index b6a181c..7f7c239 100644
--- a/examples/common/websocket-server/WebSocketServer.h
+++ b/examples/common/websocket-server/WebSocketServer.h
@@ -30,10 +30,12 @@
 public:
     CHIP_ERROR Run(chip::Optional<uint16_t> port, WebSocketServerDelegate * delegate);
     void Send(const char * msg);
+    void Stop();
 
     bool OnWebSocketMessageReceived(char * msg) override;
 
 private:
     bool mRunning;
+    struct lws_context * mContext       = nullptr;
     WebSocketServerDelegate * mDelegate = nullptr;
 };