# Piano di Implementazione: Single-Thread Communication Proxy (Fanuc) ## 1. Obiettivo Garantire che tutte le interazioni con l'oggetto `FANUC_ref` avvengano esclusivamente sul thread che ha effettuato l'inizializzazione della connessione, eliminando i crash e le instabilità causate dalla mancanza di thread-safety del driver nativo, quando il parametro `MachWLoopSingleThread` è impostato su `true`. ## 2. Diagnosi del problema (perché il pattern proxy è la soluzione) `FANUC_ref` viene toccato da almeno 3 thread diversi: - il thread di init/UI (costruttore `Fanuc` → `Runtime.CreateNC` + `tryConnect`); - il thread macchina dedicato `WorkerLoopMachine` (`AdapterForm.cs:2542`) → `doMachineTask` (`Generic.cs:723`) → metodi `process*` override di `Fanuc` che chiamano `FANUC_ref` direttamente; - il thread worker server (`Task.Run` pool) → `doServerTaskAsync` (`AdapterForm.cs:1556/1713`) e `tryConnect` (`2498`). Il `_plcLock` (SemaphoreSlim) in `AdapterForm` serializza solo i **tempi** di esecuzione tra macchina e server, **non l'affinità di thread**: chi vince il lock esegue su thread diversi nel tempo. Il driver FOCAS richiede tutte le chiamate sullo stesso thread → crash atteso. Il check `validThread` in `WorkerLoopMachine` (`2556`) verifica solo il loop macchina, non l'accesso a `FANUC_ref`, quindi non basta. **Soluzione**: marshalling di TUTTE le chiamate a `FANUC_ref` su un unico thread proprietario (`_commThread`), che è anche il thread che crea la connessione NC. ## 3. Architettura Tecnica: Pattern Proxy ### Componenti del Proxy (interni alla classe `Fanuc`): - **`_commQueue`**: `BlockingCollection` coda thread-safe delle azioni da eseguire. - **`_commThread`**: thread unico e persistente, avviato durante l'inizializzazione. Unico proprietario di `FANUC_ref` (incluso `Runtime.CreateNC`). - **`ExecuteProxy(Func)`**: marshalling sincrono per il chiamante (blocca su `tcs.Task.Result`), asincrono rispetto al thread del proxy. ### Strategia di marshalling (LEAF, non body) Ogni accesso diretto a `FANUC_ref.X(...)` viene sostituito con una chiamata a `ExecuteProxy`. Questo garantisce l'affinità di thread per ogni singola operazione FOCAS e **non introduce reentrancy** (le lambda sul comm-thread chiamano solo `FANUC_ref` direttamente, mai metodi proxied), quindi **niente deadlock da rientro**. ### Gestione dei parametri `ref`/`out` I thread non possono catturare `ref`/`out` di un **parametro di metodo** in una lambda. Per i metodi wrapper pubblici con `ref` parametro servono **helper dedicati con copy-in/copy-out**: copiano il valore in una variabile locale, marshallano la chiamata che scrive la locale, e al ritorno riscrivono la locale nel parametro. Per i `ref` a **variabili locali** (che una lambda può catturare) si usa il marshalling inline. ## 4. Modifiche ai Componenti (dettaglio) ### A. Infrastruttura (interna a `Fanuc.cs`) 1. `_commQueue` (`BlockingCollection`) + `_commThread` (campo). 2. `StartCommThread()`: crea/avvia il thread dedicato se non è vivo; crea una coda nuova se serve (riavvio). Il thread drena la coda in loop, inghiottendo le eccezioni delle singole azioni per non morire. 3. `StopCommThread()`: `CompleteAdding()` + `Join(timeout)` + `_commThread = null`. 4. `ExecuteProxy(Func)`: - se `MachWLoopSingleThread && _commThread != null && _commThread.IsAlive`: accoda l'azione e ritorna `tcs.Task.Result` (propaga le eccezioni al chiamante); - altrimenti (thread non attivo / flag off): esegue inline sotto `lock(_lockSync)` come fallback anti-hang (durante lo stop nessuna chiamata deve arrivare comunque). ### B. Lifecycle del thread (CORRETTO rispetto alla versione precedente) Il thread dedicato deve essere **lo stesso che crea FANUC_ref** (affinità FOCAS). Non va mai fermato/riavviato a metà vita dell'oggetto, altrimenti il nuovo thread ≠ thread creatore. - **Costruttore**: `StartCommThread()` PRIMA di `Runtime.CreateNC`; la creazione NC + assegnazione `FANUC_ref` vengono marshallate sul comm-thread. - **`startAdapter` override**: se `_commThread` non è vivo → `StartCommThread()` + ricreazione NC marshallata sul nuovo thread (copre il path stop→start sullo stesso oggetto senza duplicare CreateNC sul path di avvio normale); poi `base.startAdapter`. - **`stopAdapter` override**: `await base.stopAdapter(...)` (che marshalla la `Disconnect`) poi `StopCommThread()` (nessun leak sul restart che ricrea l'oggetto). ### C. Rifattorizzazione dei metodi di I/O Sostituzione delle chiamate dirette a `FANUC_ref` con il proxy: - `FANUC_ref.Connected` → `ExecuteProxy(() => FANUC_ref.Connected)` (incluso dentro le stringhe di log interpolate). - `F_RW_*`, `F_Read_macro`, `getAllDynData`, `getSysInfo`, `getPrgNameMain`, `Is30Series` con `ref` a **variabili locali** → `ExecuteProxy(() => FANUC_ref.X(..., ref locale))`. - `Connect`/`Disconnect` con `ref` locale → `ExecuteProxy(() => { FANUC_ref.X(ref l); return true; })`. - **4 helper copy-in/out** per i wrapper pubblici con `ref` **parametro**: `ProxyMacroShort`, `ProxyMacroDouble` (per `FanucMemMacroRW`), `ProxyByte`, `ProxyByteArr` (per `FanucMemRW` byte/byte[]). ## 5. Validazione e Test - Log: tutte le chiamate a `FANUC_ref` devono presentare lo stesso `ManagedThreadId` del comm-thread (FANUC-Comm-Thread), NON del WorkerLoopMachine. - Test di connessione: `tryConnect` deve funzionare attraverso il proxy. - Test stop→start e restart: verificare che `_commThread` venga ricreato con ricreazione NC e che non ci siano double-create sull'avvio normale. ## 6. Note di sicurezza (pitfall) - **Reentrancy**: con marshalling leaf non c'è rientro, ma `ExecuteProxy` resta difensivo. - **Hang**: il fallback inline sotto lock evita `tcs.Task.Result` che si blocca se il thread è morto. - **Shutdown**: `StopCommThread()` su `stopAdapter`; niente `CompleteAdding` durante il run. - **`MachWLoopSingleThread`** è forzato a `true` nel costruttore (`Fanuc.cs:67`): il ramo proxy è sempre attivo per FANUC; il ramo `else` con lock è un fallback di sicurezza.