Windows-Tray-Anwendung (CLMgr-Anbindung an SwyxIt!) samt WebSocket-Zugang, Firefox-Erweiterung und Browser-Beispielclient.
29 KiB
SwyxTray
Windows-Anwendung ohne Fenster, die nur ein Symbol im Infobereich der Taskleiste (unten rechts) anzeigt und sich per Swyx Client SDK mit dem lokal laufenden SwyxIt!-Client verbindet.
Was die App tut
-
Verbindet sich beim Start mit dem Client Line Manager (
CLMgr.exe) der aktuellen Windows-Sitzung — es wird kein zweiter Client gestartet, sondern der bereits laufende mitbenutzt. -
Zeigt den Leitungszustand über die Farbe des Tray-Symbols:
Farbe Zustand grau SwyxIt! nicht verbunden rot SwyxServer nicht erreichbar grün bereit, keine belegte Leitung blau eingehender Ruf türkis Verbindungsaufbau (ausgehend) orange Gespräch aktiv violett Gespräch gehalten -
Blendet bei eingehenden Rufen eine Sprechblase mit Name und Rufnummer ein.
-
Stellt einen WebSocket-Zugang auf 127.0.0.1 bereit, über den eine im Browser laufende Seite wählen, annehmen und auflegen kann und im Gegenzug jede Zustandsänderung zugestellt bekommt (siehe unten).
-
Kontextmenü (Rechtsklick): Status, belegte Leitungen, WebSocket-Zugang, Lese Tabs, Tab öffnen, Tab schliessen, Neu verbinden, Protokoll öffnen, Beenden. Doppelklick zeigt den Status als Sprechblase.
Die drei Tab-Punkte laufen über den Tab-Verwaltungskanal zum Firefox-Plugin (siehe unten); ohne verbundenes Plugin (oder mit
PluginPort: 0) bleiben sie ohne Ergebnis bzw. grau. Lese Tabs holt die Titel der offenen Tabs und zeigt sie als Sprechblase — die vollständige Liste samt Adressen landet im Protokoll. Tab öffnen öffnet einen Tab mithttps://www.assecutor.de, Tab schliessen schließt alle Tabs, deren Titel „Assecutor Data Service GmbH" enthält — als Komposition auslistundcloseje Treffer, wie beimfocus-Kommando (beide Werte sind Konstanten in TrayApplicationContext.cs).
Verwendetes SDK
Offizielles NuGet-Paket von Enreach:
<PackageReference Include="Swyx.Client.ClmgrAPI" Version="14.21.0" />
Das Paket enthält die Interop-Wrapper der ClientLineManager-COM-API
(IpPbx.CLMgrLib) und wählt über $(Platform) automatisch Interop.CLMgr.dll
(x86) bzw. Interop64.CLMgr.dll (x64). Deshalb muss Platform gesetzt sein
— bei AnyCPU bricht der Build mit einem Fehler des Pakets ab.
Die Version 14.21.0 passt zur hier installierten SwyxIt!-Version 14.21.4345.0. Bei einem Client-Update die Paketversion mitziehen.
Genutzte Schnittstellen:
IClientLineMgrDisp—DispInit,DispRegisterUser,DispNumberOfLines,DispGetLine,DispSelectedLineNumber,DispIsServerUp,DispGetCurrentUser,DispGetCurrentServer,DispReleaseUserIClientLineDisp—DispState,DispPeerNumber,DispPeerNameIClientLineMgrEventsPub_Event—PubOnLineMgrNotification(msg, param)CLMgrLineStates— die Leitungszustände als typisierte Aufzählung
Aufbau
| Datei | Zweck |
|---|---|
| Program.cs | Einstieg, STA-Thread, Einzelinstanz-Sperre, Fehlerprotokollierung |
| TrayApplicationContext.cs | NotifyIcon, Kontextmenü, Sprechblasen |
| TrayIcons.cs | erzeugt die Symbole zur Laufzeit (keine .ico-Dateien nötig) |
| Swyx/SwyxClient.cs | CLMgr-Verbindung, Ereignisbehandlung, Zustandsabfrage |
| Swyx/SwyxModels.cs | SwyxSnapshot, SwyxLineInfo, Gesamtzustand |
| Swyx/LineStateExtensions.cs | Einordnung und deutscher Klartext der CLMgrLineStates |
| Swyx/PubCLMgrMessages.cs | Meldungsnummern aus CLMgrPubTypes.h |
| Web/LocalWebSocketServer.cs | Listener auf 127.0.0.1 und ::1, Verbindungsverwaltung, Verteilen der Zustände |
| Web/WebSocketHandshake.cs | HTTP-Upgrade, Prüfung von Token, Origin und Host |
| Web/WebSocketSession.cs | eine Verbindung: Empfangsschleife, serialisiertes Senden |
| Web/CommandExecutor.cs | Kommandos → SwyxClient, mit Zeitgrenze |
| Web/UiDispatcher.cs | Sprung aus den Netzwerk-Threads in den UI-Thread |
| Web/Protocol.cs | die JSON-Nachrichten |
| Web/WebSocketConfig.cs | Port, Token, erlaubte Origins |
| examples/browser-client.html | Beispielseite zum Erproben des Zugangs |
| src/SwyxFFPlugin/ | die Firefox-Erweiterung: manifest.json und background.js |
| Log.cs | Dateiprotokoll unter %LOCALAPPDATA%\SwyxTray\swyxtray.log |
Zwei Entwurfsentscheidungen
Ereignisgesteuert statt Polling. PubOnLineMgrNotification liefert jede
Zustandsänderung sofort. Bei jeder Meldung wird der komplette Zustand neu
gelesen — das kostet nur wenige COM-Zugriffe und macht die Anzeige unabhängig
davon, ob eine Meldungsnummer richtig zugeordnet ist. PubCLMgrMessages steckt
nämlich nur im C-Header des SDK, nicht in der TypeLib. Zusätzlich läuft alle
5 Sekunden ein Sicherheitszyklus, der einen beendeten CLMgr erkennt und die
Verbindung wiederherstellt.
Alles im UI-Thread. CLMgr ist ein STA-COM-Server; der WinForms-Thread ist STA und hat bereits eine Nachrichtenschleife. Dadurch werden die COM-Ereignisse automatisch dorthin gemarshallt — kein manuelles Marshalling, keine Sperren.
CoInitializeSecurity ist nicht optional
CLMgr.exe ist ein eigener Prozess. Beim Anmelden der Ereignissenke ruft er in
diesen Prozess zurück — und ohne eigene Sicherheitsvorgabe weist COM diesen
Rückruf mit E_ACCESSDENIED ab. NativeMethods.InitializeComSecurity
setzt deshalb vor dem ersten COM-Aufruf RPC_C_AUTHN_LEVEL_NONE /
RPC_C_IMP_LEVEL_IDENTIFY. Das betrifft nur diesen Prozess, der außer der
Ereignissenke nichts nach außen anbietet. Wer es enger fassen will, probiert
RPC_C_AUTHN_LEVEL_CONNECT (2) — das genügt, solange CLMgr in derselben Sitzung
unter demselben Konto läuft.
Schlägt das Anmelden trotzdem fehl, ist das kein Abbruchgrund: die App bleibt verbunden und schaltet den Abfragezyklus von 5 s auf 1 s herunter.
Zugriff aus dem Browser (WebSocket)
Die App hört auf ws://127.0.0.1:17654/ und spricht dort JSON — eine
Nachricht je Frame. Über dieselbe Verbindung laufen beide Richtungen: die Seite
schickt Kommandos, die App schickt unaufgefordert jede Zustandsänderung.
Einrichten
Beim ersten Start entsteht %LOCALAPPDATA%\SwyxTray\websocket.json:
{
"Enabled": true,
"Port": 17654,
"PluginPort": 17655,
"Token": "E59BF4…",
"AllowedOrigins": [],
"MaxConnections": 8,
"AllowRemoteAccess": false
}
Solange die Prüfungen abgeschaltet sind (siehe unten), sind Token und
AllowedOrigins ohne Wirkung — für einen ersten Test ist an dieser Datei nichts
zu tun. Der Rest dieses Abschnitts gilt für den eingeschalteten Zustand.
Das Token wird zufällig erzeugt, die Liste der Origins bleibt leer — ohne
Eintrag wird jede Verbindung aus einem Browser abgewiesen. Das ist Absicht,
denn ein offener Port ohne diese Schranke hieße: jede beliebige Seite, die
jemand im Browser öffnet, kann Anrufe auslösen. Einzutragen ist der Ursprung
der aufrufenden Seite, also z. B. https://crm.example.local — ohne Pfad. Nach
einer Änderung muss die App neu gestartet werden.
WebSocket-Zugang → Verbindungsdaten kopieren im Kontextmenü legt Adresse und Token in die Zwischenablage.
Verbinden
const ws = new WebSocket("ws://127.0.0.1:17654/?token=…", ["swyxtray.v1"]);
Das Token darf statt in der URL auch als zweites Unterprotokoll stehen
(["swyxtray.v1", "token.…"]) — nützlich, wenn das Geheimnis nicht in
Adresszeilen und Verlaufslisten auftauchen soll. Die WebSocket-API des Browsers
kennt keine eigenen Header, deshalb diese beiden Wege.
Ein vollständiges Beispiel mit Wiederverbindung steht in examples/browser-client.html.
Firefox-Plugin (SwyxFFPlugin): der Tab-Verwaltungskanal
Für das Firefox-Plugin hört die App zusätzlich auf einem zweiten Port —
voreingestellt 17655 (PluginPort in websocket.json, 0 schaltet ihn
ab; in einer bestehenden Datei ohne diesen Eintrag gilt automatisch 17655).
Auf diesem Port läuft ausschließlich die Tab-Verwaltung — keine
Snapshots, keine Anruf-Ereignisse, keine Telefonie-Kommandos (sie werden mit
ok: false abgewiesen). Nach dem hello ist die Richtung umgekehrt zum
Hauptport: SwyxTray fragt, das Plugin antwortet. Es gibt drei Aufträge,
jeder wird mit einem tabresult samt unveränderter id beantwortet:
Offene Tabs auslesen:
→ an das Plugin: {"type":"tab","id":1,"action":"list"}
← vom Plugin: {"cmd":"tabresult","id":1,"ok":true,
"tabs":[{"id":42,"title":"Kundenakte Muster GmbH",
"url":"https://crm.example.local/kunden/4711","active":false}]}
Tab mit einer URL öffnen (der neue Tab ist in Firefox von selbst vorn):
→ an das Plugin: {"type":"tab","id":2,"action":"open","url":"https://crm.example.local/kunden/4711"}
← vom Plugin: {"cmd":"tabresult","id":2,"ok":true,"tabId":43}
Tab schließen:
→ an das Plugin: {"type":"tab","id":3,"action":"close","tabId":42}
← vom Plugin: {"cmd":"tabresult","id":3,"ok":true}
Antwortet das Plugin binnen fünf Sekunden nicht (oder ist keines verbunden),
gilt der Auftrag als gescheitert; die darauf aufbauenden Kommandos des
Hauptports (focus, tabs, opentab, closetab) enden dann mit
ok: false. Vom Plugin aus sind nur tabresult und ping zulässig. Ein
„Tab aktivieren“ kennt der Kanal bewusst nicht — beim focus-Kommando holt
SwyxTray einen vorhandenen Tab als Komposition nach vorn: list, passenden
Tab per close schließen und die URL per open neu öffnen.
Das Plugin selbst liegt in src/SwyxFFPlugin/ —
background.js setzt genau diese drei
Aufträge um und verbindet sich selbsttätig neu (1 s Abstand, je Fehlversuch
verdoppelt bis 30 s), falls SwyxTray noch nicht oder nicht mehr läuft.
Installation zum Erproben: about:debugging#/runtime/this-firefox →
Temporäres Add-on laden… → die manifest.json des Ordners wählen (Näheres
im README des Plugins).
Sind die Prüfungen wieder eingeschaltet, braucht die Adresse zusätzlich das
Token (ws://127.0.0.1:17655/?token=…), und in AllowedOrigins muss der
Origin des Plugins stehen. Firefox sendet dafür moz-extension://<UUID>,
wobei die UUID je Installation zufällig vergeben wird — sie steht in
about:debugging unter „Interne UUID“ der Erweiterung.
Kommandos (Seite → App)
| Kommando | Felder | Wirkung |
|---|---|---|
call |
number |
wählt auf einer freien Leitung |
answer |
line (optional) |
nimmt an; ohne Angabe die klingelnde Leitung |
hangup |
line (optional) |
legt auf; ohne Angabe die belegte Leitung |
hold |
line (optional) |
stellt das Gespräch in die Warteschleife |
focus |
url, title (optional) |
aktiviert oder öffnet den passenden Firefox-Tab über das Plugin |
tabs |
— | liest die offenen Firefox-Tabs über das Plugin aus |
opentab |
url |
öffnet über das Plugin einen neuen Firefox-Tab |
closetab |
tabId |
schließt über das Plugin den Tab mit dieser Id |
status |
— | fordert den vollen Zustand an |
reconnect |
— | verwirft die CLMgr-Verbindung und baut sie neu auf |
ping |
— | Lebenszeichen |
id ist frei wählbar und steht in der Antwort wieder da — anders als bei HTTP
gibt es keine eingebaute Zuordnung von Anfrage und Antwort.
→ {"id":7,"cmd":"call","number":"+49 30 1234567"}
← {"type":"result","id":7,"ok":true,"line":2}
Leitungen werden nach außen 1-basiert gezählt, so wie sie im Kontextmenü und
in SwyxIt! erscheinen. Rufnummern dürfen Leerzeichen, Bindestriche, Punkte und
Klammern enthalten; alles andere außer Ziffern, führendem +, * und # wird
abgewiesen, statt es an CLMgr weiterzureichen.
Ein ok: true auf call heißt nur, dass CLMgr den Auftrag angenommen hat. Das
Wählen läuft asynchron — ob der Ruf zustande kommt, steht erst in den folgenden
Zustandsmeldungen.
focus (seit Protokollversion 3, ab Version 5 ausschließlich über das
Firefox-Plugin, siehe unten): war ein passender Tab schon offen — sein
Titel enthält den title oder seine Adresse ist die url —, wird er
geschlossen und die url neu geöffnet ("focused": true), sonst nur neu
geöffnet ("focused": false). Ist kein Plugin verbunden oder antwortet es
nicht, endet das Kommando mit ok: false — einen anderen Weg, eine Seite
anzuzeigen, gibt es nicht. Die url ist Pflicht, title dient nur dem
Tab-Abgleich; erlaubt sind nur absolute http(s)-Adressen.
→ {"id":9,"cmd":"focus","title":"Kundenakte Muster GmbH","url":"https://crm.example.local/kunden/4711"}
← {"type":"result","id":9,"ok":true,"focused":false}
tabs, opentab, closetab (seit Protokollversion 6): die drei Aufträge des
Tab-Verwaltungskanals stehen damit auch Webseiten offen — SwyxTray reicht sie
unverändert an das Firefox-Plugin durch. Ist kein Plugin verbunden oder
antwortet es nicht binnen fünf Sekunden, endet das Kommando mit ok: false.
Für opentab gilt dieselbe Regel wie bei focus: nur absolute
http(s)-Adressen. Die tabId für closetab stammt aus einer vorherigen
tabs-Antwort; nach Öffnen oder Schließen verschickt die App keine neue
Tab-Liste von selbst — wer sie aktuell braucht, fragt mit tabs nach:
→ {"id":10,"cmd":"tabs"}
← {"type":"result","id":10,"ok":true,
"tabs":[{"id":42,"title":"Kundenakte Muster GmbH",
"url":"https://crm.example.local/kunden/4711","active":false}]}
→ {"id":11,"cmd":"opentab","url":"https://crm.example.local/kunden/4711"}
← {"type":"result","id":11,"ok":true,"tabId":43}
→ {"id":12,"cmd":"closetab","tabId":43}
← {"type":"result","id":12,"ok":true}
Meldungen (App → Seite)
Direkt nach dem Verbinden kommen hello und ein snapshot, danach ein
snapshot bei jeder Zustandsänderung. Die Seite muss also nie pollen und
nie selbst Zustand mitführen:
{"type":"snapshot","connected":true,"serverUp":true,"overall":"Ringing",
"statusText":"Eingehender Ruf","user":"…","server":"…",
"lines":[{"line":1,"state":"LSRinging","stateCode":3,"stateText":"klingelt",
"peer":"Muster GmbH (+493012345)","peerNumber":"+493012345",
"peerName":"Muster GmbH","busy":true,"selected":true}]}
overall ist derselbe Gesamtzustand, den auch die Farbe des Tray-Symbols zeigt.
Zusätzlich zum snapshot meldet die App Anruf-Ereignisse als eigene
Nachricht vom Typ call — damit muss eine Seite keine zwei Zustände
vergleichen, um einen eingehenden Ruf zu erkennen:
event |
Bedeutung |
|---|---|
incoming |
Leitung beginnt zu klingeln (auch Anklopfen) |
outgoing |
Wahl eines ausgehenden Rufs beginnt |
connected |
Gespräch steht; direction sagt incoming/outgoing, sofern erkennbar |
ended |
Leitung ist wieder frei; Peer-Angaben aus dem letzten belegten Zustand |
{"type":"call","event":"incoming","line":1,
"peer":"Muster GmbH (+493012345)","peerNumber":"+493012345","peerName":"Muster GmbH"}
Die Reihenfolge ist garantiert: erst der snapshot, unmittelbar danach die
daraus abgeleiteten call-Ereignisse. Ein typischer Ablauf für ein
angenommenes Gespräch ist also incoming → connected (mit
"direction":"incoming") → ended, jeweils mit einem snapshot davor.
Wählt die Seite selbst per Kommando call, folgen outgoing → connected →
ended auf demselben Weg. Diese Ereignisse gibt es seit Protokollversion 2
(Feld protocol im hello).
Was den Zugang absichert
Zur Zeit greift davon nur die Bindung. Token-, Origin- und Host-Prüfung sind für die Erprobung abgeschaltet, und
AllowRemoteAccesssteht auftrue— siehe die beiden folgenden Abschnitte. Der Rest dieses Abschnitts beschreibt den eingeschalteten Zustand.
Vier Schranken, die zusammen wirken:
- Gebunden nur an 127.0.0.1 und ::1. Von außen ist der Port nicht
erreichbar, und die Windows-Firewall fragt beim Start nichts nach. Diese
Schranke fällt mit
AllowRemoteAccess(siehe unten). - Token in jedem Handshake, zeitkonstant verglichen.
- Origin-Allowlist. Für WebSockets gilt die Same-Origin-Policy nicht — der Browser blockiert nichts, es gibt keinen Preflight. Diese Prüfung ist damit nicht Härtung, sondern die eigentliche Schranke gegen fremde Seiten. Fehlt der Origin-Header ganz, ist der Aufrufer kein Browser; das ist zulässig, weil das Token bereits nachgewiesen wurde. Eine Webseite kann den Header nicht weglassen.
- Host-Prüfung auf
127.0.0.1,localhostbzw.[::1]. Ohne sie könnte eine Seite einen eigenen Namen auf 127.0.0.1 zeigen lassen (DNS-Rebinding). Entfällt ebenfalls mitAllowRemoteAccess.
Zugriff von einem anderen Rechner
Mit "AllowRemoteAccess": true bindet die App statt an die Loopback-Adressen an
alle Schnittstellen (0.0.0.0 und ::) und ist damit im lokalen Netz
erreichbar:
new WebSocket("ws://192.168.180.135:17654/?token=…");
Die erreichbaren Adressen stehen beim Start im Protokoll, und Verbindungsdaten kopieren liefert dann ebenfalls die LAN-Adresse statt 127.0.0.1.
Drei Dinge ändern sich damit:
-
Die Host-Prüfung entfällt. Unter welchem Namen oder welcher Adresse ein entfernter Rechner die App anspricht, kann sie nicht wissen. Der Schutz gegen DNS-Rebinding ruht damit allein auf Token und Origin-Liste.
-
Die Windows-Firewall redet mit. Ist sie aktiv, braucht es eine eingehende Regel, sonst kommt vom anderen Rechner nichts an:
New-NetFirewallRule -DisplayName "SwyxTray WebSocket" -Direction Inbound ` -Protocol TCP -LocalPort 17654 -Action AllowDas erfordert Administratorrechte — die einzige Stelle des Vorhabens, an der sie nötig sind, und ein Grund, warum Loopback die Voreinstellung bleibt.
-
ws://gilt aus der Ferne nicht mehr als vertrauenswürdig. Die Ausnahme für Mixed Content gilt nur für Loopback. Eine HTTPS-Seite auf dem anderen Rechner kannws://192.168.…also nicht öffnen — für den Test muss die Seite überhttp://kommen, oder es brauchtwss://mit einem Zertifikat, das die App derzeit nicht anbietet.
Wer den Zugang nicht nur zum Testen aus dem Netz erreichbar macht, sollte sich bewusst sein: Token und Origin-Liste sind dann das Einzige, was zwischen dem Netz und dem Telefon des Benutzers steht.
Zur Zeit sind die Prüfungen abgeschaltet
Für die Erprobung entfallen Token-, Origin- und Host-Prüfung — in jedem
Build. Eine Seite verbindet sich ohne jede Einrichtung, auch direkt aus dem
Dateisystem, wo der Browser den Origin null sendet:
new WebSocket("ws://127.0.0.1:17654/");
Die Konfigurationsdatei wird nur noch für Port, Verbindungsgrenze und
AllowRemoteAccess gelesen; ein fehlendes Token verhindert den Start nicht.
Zusammen mit dem eingeschalteten Zugriff aus dem Netz heißt das: jeder im selben Netz kann über diesen Anschluss telefonieren und mitlesen, wer anruft. In diesem Zustand nicht ausliefern.
Erkennbar ist das am Protokoll und am Kontextmenü, das dann WebSocket: Port 17654, 1 Verbindung(en) — im Netz — OHNE PRUEFUNG zeigt:
[INF] WebSocket-Zugang aus dem Netz erreichbar unter ws://192.168.180.135:17654/
— OHNE PRUEFUNG: Token, Origin und Host werden nicht geprueft. Jeder im
selben Netz kann telefonieren und mitlesen. Nicht in diesem Zustand
ausliefern.
Wieder einschalten lässt sich alles mit einer Zeile in Web/WebSocketConfig.cs — die Prüfungen selbst stehen unverändert in Web/WebSocketHandshake.cs:
public static readonly bool SecurityDisabled = true; // <- zur Zeit
Der dort im Kommentar hinterlegte Ersatz macht daraus wieder ein
#if DEBUG — dann prüft der Release-Build alles, während sich der Debug-Build
weiterhin ohne Einrichtung erproben lässt.
Was im Browser noch dazwischenkommen kann
ws://127.0.0.1 gilt als vertrauenswürdiger Ursprung und wird auch aus einer
HTTPS-Seite heraus nicht als Mixed Content blockiert. Neuere Chrome-Versionen
beschränken allerdings Zugriffe aus dem öffentlichen ins lokale Netz (Local
Network Access) und fragen den Benutzer — das lässt sich per
Unternehmensrichtlinie freigeben, aber nicht durch einen Antwort-Header lösen.
Vor dem Rollout mit der IT klären.
Browser lösen localhost häufig zuerst nach ::1 auf. Die App hört deshalb auf
beiden Loopback-Adressen; sonst funktionierte ws://127.0.0.1:…, während
ws://localhost:… ohne erkennbaren Grund scheiterte.
Zwei Entwurfsentscheidungen
Kein HttpListener. Der läuft über http.sys und braucht auch für
127.0.0.1 eine einmalige URL-Reservierung mit Administratorrechten
(netsh http add urlacl). Die App soll ohne solche Rechte auskommen, deshalb
ein schlichter TcpListener. Vom WebSocket-Protokoll bleibt dabei nur der
HTTP-Upgrade von Hand zu schreiben — Rahmen, Maskierung, Ping/Pong und der
Schließvorgang kommen von WebSocket.CreateFromStream aus der
Klassenbibliothek.
Kommandos laufen über den UI-Thread. CLMgr ist ein STA-COM-Server, und die App bündelt alle COM-Zugriffe im WinForms-Thread. Der UiDispatcher schleust jedes Kommando aus dem Netzwerk-Thread dorthin, mit einer Zeitgrenze von 10 Sekunden: bliebe die Nachrichtenschleife hängen, fröre die Verbindung sonst stumm ein.
Bauen und Starten
cd src\SwyxTray
dotnet build -c Release
.\bin\x64\Release\net10.0-windows\win-x64\SwyxTray.exe --verbose
--verbose schreibt zusätzlich jede CLMgr-Meldung ins Protokoll — für die
erste Inbetriebnahme empfehlenswert.
Aus VS Code (F5)
Die Startkonfiguration „SwyxTray debuggen (Desktop-Sitzung)" in .vscode/launch.json baut, startet und hängt den Debugger an. Sie startet die App über eine geplante Aufgabe (.vscode/start-desktop.ps1) — nötig, weil bei einer Remote-Verbindung (SSH, Tunnel) der Debugger Prozesse sonst in der Sitzung des VS-Code-Servers startet, meist Sitzung 0. Dort gibt es keine Taskleiste, die App läuft dann ohne sichtbares Tray-Symbol. Die geplante Aufgabe läuft dagegen mit dem interaktiven Token des Benutzers und landet auf dem Desktop; danach hängt sich der Debugger per Prozessname an. Eine bereits laufende Instanz wird vorher beendet, sonst griffe die Einzelinstanz-Sperre.
Autostart: eine Verknüpfung auf die EXE in
shell:startup (%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup)
ablegen. Kein Dienst, keine Administratorrechte.
Voraussetzungen
-
SwyxIt! muss laufen (getestet gegen 14.21.4345.0).
-
.NET-10-Desktop-Laufzeit. Auf diesem Rechner ist nur .NET 10 installiert, deshalb steht
net10.0-windowsin der csproj. Für .NET 8 einfach<TargetFramework>net8.0-windows</TargetFramework>setzen. Alternativ ohne Laufzeitabhängigkeit ausliefern:dotnet publish -c Release -r win-x64 --self-contained true -
Die App läuft in derselben Windows-Sitzung wie SwyxIt! — als normale Benutzeranwendung, nicht als Dienst (siehe unten).
Warum kein Windows-Dienst
Ursprünglich war ein Windows-Dienst vorgesehen. Das funktioniert für diesen
Zweck nicht: ein Dienst läuft in Session 0, der SwyxIt!-Client und sein
CLMgr.exe in der Benutzersitzung (1, 2, …). Über die Sitzungsgrenze hinweg
lässt sich der laufende Client nicht ansprechen. Ein Dienst müsste dafür einen
Hilfsprozess per CreateProcessAsUser in die Benutzersitzung starten und mit
ihm über eine Named Pipe reden. Die Tray-Anwendung umgeht das Problem
vollständig, weil sie von vornherein in der richtigen Sitzung läuft.
Stand der Prüfung
- Übersetzung gegen das echte SDK: erfolgreich. Alle verwendeten Typen und
Signaturen stammen aus
Interop64.CLMgr.dll(Version 2.0.0.0) des Pakets und wurden gegen die installierte TypeLibCLMgr 2.0({F8E552F7-4C00-11D3-80BC-00105A653379}, registriert aufC:\Program Files (x86)\Swyx\SwyxIt!\CLMgr.exe) verifiziert. Gegen den echten, laufenden Client verifiziert (SwyxIt! 14.21.4345.0):
[INF] SwyxTray gestartet (PID 3348, Sitzung 0, x64).
[INF] Mit CLMgr verbunden (UserId 0, Benutzer 'Assecutor, G.Timm',
Server '127.0.0.1', 4 Leitungen).
[DBG] Zustand: Idle | L1=LSInactive(0) * | L2=LSInactive(0)
| L3=LSInactive(0) | L4=LSInactive(0)
Bestätigt sind damit: COM-Aktivierung, DispInit, DispRegisterUser, Anmelden
der Ereignissenke, Auslesen aller vier Leitungen samt Zustand und ausgewählter
Leitung, Ableitung des Gesamtzustands, Symbolerzeugung, Änderungserkennung
(keine Doppelmeldungen über mehrere Zyklen) und das Dateiprotokoll.
WebSocket-Zugang gegen die laufende App geprüft (mit einem
ClientWebSocket als Gegenstelle):
[INF] WebSocket-Zugang bereit auf ws://127.0.0.1:17654/
[INF] WebSocket-Anfrage von 127.0.0.1:51312 abgewiesen (403): Token fehlt oder ist falsch.
[INF] WebSocket-Anfrage von 127.0.0.1:51313 abgewiesen (403): Origin 'https://boese.example'
ist nicht freigegeben.
[INF] WebSocket 1 verbunden (127.0.0.1:51314, Origin (ohne Origin)).
[INF] WebSocket 2 verbunden ([::1]:64293, Origin https://test.example).
Bestätigt sind damit: Handshake samt Unterprotokoll-Aushandlung, Token- und
Origin-Prüfung in beide Richtungen, Erreichbarkeit über 127.0.0.1 und
localhost/::1, hello und snapshot beim Verbinden, die Kommandos ping,
status, hangup, das Abweisen ungültiger Rufnummern, unbekannter Kommandos
und kaputten JSONs sowie der Schließvorgang.
Geprüft wurde außerdem der jetzige Zustand mit abgeschalteten Prüfungen: der Release-Build nimmt eine Verbindung über die LAN-Adresse ohne Token und mit fremdem Origin an. Zuvor, mit eingeschalteten Prüfungen, wies er dieselbe Anfrage mit 403 ab, während der Debug-Build sie annahm — beide Zweige sind also erprobt.
Mit "AllowRemoteAccess": true lauscht die App auf 0.0.0.0:17654 und
[::]:17654 (bestätigt über Get-NetTCPConnection); eine Verbindung über die
LAN-Adresse ws://192.168.180.135:17654/ wird angenommen, dieselbe mit falschem
Token weiterhin abgewiesen. Der Test lief vom selben Rechner aus — dass ein
entfernter Rechner durchkommt, hängt zusätzlich an der Firewall. Auf dem
Testrechner ist sie in allen Profilen abgeschaltet, dort war keine Regel nötig.
Noch nicht am realen Verkehr geprüft: Zustandswechsel während eines echten
Gesprächs — Klingeln, Annehmen, Halten, Auflegen — und damit die Sprechblase bei
eingehenden Rufen, der Farbwechsel des Symbols und das Verteilen der
Zustandsmeldungen an verbundene Seiten. Dafür ist ein Testanruf nötig; call
wurde bewusst nur mit ungültigen Nummern erprobt, um keinen echten Ruf
auszulösen. Ebenso ungeprüft ist das Symbol im Infobereich selbst, weil die
verwendete Konsole in Session 0 lief und dort keine Taskleiste existiert — und
damit auch die Beispielseite in einem echten Browser.
Zwei Hürden auf dem Weg dorthin
Beide betrafen die Umgebung, nicht den Code, sind aber für die Auslieferung relevant:
- Smart App Control blockierte den COM-Proxy. Jedes
QueryInterfaceüber die Prozessgrenze lädtC:\Program Files\Common Files\Swyx\CLMgrPs64.dllin den eigenen Prozess; genau diese Swyx-Datei wurde abgewiesen (0x800711C7, CodeIntegrity 3033/3077/3118, Enterprise signing level requirements). Das Abschalten von Smart App Control wirkt erst nach einem Neustart — der Registry-Wert steht sofort auf0,Win32_DeviceGuardmeldet aber bis zum Reboot weiter Erzwingung. Läuft beim Kunden WDAC, mussCLMgrPs64.dllim Regelwerk freigegeben sein — das trifft jede Anwendung, die den Swyx-Proxy lädt. IConnectionPoint::Advisescheiterte mitE_ACCESSDENIED— gelöst durchCoInitializeSecurity(siehe oben).