Les pièges des lecteurs réseau et des chemins UNC — Travailler avec un serveur de fichiers (dossier partagé) depuis une application métier

· · Lecteur réseau, Chemin UNC, SMB, Partage de fichiers, Service Windows, Serveur de fichiers, C#, .NET, Réseau, Investigation de bugs, Développement Windows, Conseil technique

« Ça fonctionnait sur le poste de développement, mais chez le client on obtient “Le chemin Z:\ est introuvable.” » « Dès qu’on l’a mis sur le Planificateur de tâches, l’écriture vers le dossier partagé s’est mise à échouer. » « Depuis qu’on en a fait un service Windows, le serveur de fichiers est devenu invisible. » — Dans les consultations autour des applications métier, les incidents liés à un serveur de fichiers (dossier partagé) figurent parmi les grands classiques. Import des données de commande, sortie de rapports ou de CSV, surveillance des fichiers déposés par un équipement : dans les systèmes métier on-premise, le dossier partagé reste aujourd’hui un socle d’intégration bien vivant.

Ce qui rend ce type d’incident si pénible, c’est que dans la majorité des cas, il ne s’agit pas d’un bug de code : la cause est enracinée dans des mécanismes propres à Windows — la relation entre lettres de lecteur et sessions d’ouverture de session, le compte d’exécution du service et l’authentification, ou encore la gestion des connexions SMB. Suivre le code pas à pas dans un débogueur ne mène nulle part, et le temps s’écoule pendant que « ça ne se reproduit pas sur mon PC ».

Cet article s’adresse aux développeurs d’applications métier (WinForms/WPF/services Windows) qui implémentent l’écriture, l’import ou la surveillance de fichiers vers un dossier partagé. Il reprend depuis les fondamentaux les pièges des lecteurs réseau et des chemins UNC, et rassemble les règles pratiques dans un tableau de décision.

1. La conclusion d’abord

  • Une lettre de lecteur (Z: et assimilées) n’est pas une ressource globale au système — elle appartient à une session d’ouverture de session unique. Chaque session d’ouverture de session dispose de son propre jeu complet de lettres de lecteur de A à Z ; un lecteur mappé par un utilisateur est donc invisible pour un processus exécuté sous un autre utilisateur, ou pour un service exécuté sous une autre session d’ouverture de session.1
  • Lorsque l’UAC est activé pour un administrateur, deux sessions d’ouverture de session sont créées — une standard et une élevée — et les mappages de lecteurs (liens symboliques DosDevices) sont indépendants d’une session à l’autre. C’est pourquoi « Z: disparaît uniquement depuis l’application élevée ». Un contournement existe via la valeur de registre EnableLinkedConnections, qui partage les mappages entre les deux sessions, mais Microsoft documente explicitement ce réglage comme non pris en charge et « susceptible de rendre le système moins sûr ».23
  • Lorsqu’un service (ou tout processus s’exécutant dans un contexte de sécurité différent) doit atteindre une ressource distante, la recommandation officielle est d’utiliser un chemin UNC (\\serveur\partage\...). Mapper une lettre de lecteur depuis l’intérieur d’un service via net use ou les API WNet n’est pas recommandé, pour des raisons d’exposition des identifiants et d’interférence entre services.1
  • La détermination de qui doit recevoir les permissions côté partage dépend du compte d’exécution du service. LocalSystem et NetworkService s’authentifient sur le réseau en tant qu’« identifiants propres de l’ordinateur », tandis que LocalService présente des identifiants anonymes et est inadapté à l’accès à un partage. Un service exécuté sous un compte utilisateur local ne peut absolument pas atteindre de ressources réseau. En pratique, les options réellement exploitables sont un compte de domaine, ou un gMSA qui délègue la gestion du mot de passe au système d’exploitation.45678
  • On ne peut pas maintenir simultanément des connexions vers le même serveur avec deux jeux d’identifiants différents. La seconde connexion échoue avec l’erreur 1219 (ERROR_SESSION_CREDENTIAL_CONFLICT), et ceci est voulu (by design).910
  • « Lent, coupé, parfois injoignable » est le régime normal d’un dossier partagé, pas un cas limite. Une connexion inactive est déconnectée par le serveur au bout de 15 minutes par défaut (et reconnectée silencieusement au prochain accès). File.Exists renvoie false en cas de permissions insuffisantes ou d’autres erreurs, sans lever d’exception, et ne peut donc pas distinguer « le fichier n’existe pas » de « le serveur est injoignable ».1112
  • FileSystemWatcher prend bien en charge la surveillance d’un lecteur réseau ou d’un ordinateur distant, mais il faut concevoir en tenant compte des événements manqués. Un débordement de tampon peut faire perdre des événements, et lors d’une surveillance via le réseau, la taille du tampon interne est plafonnée à 64 Ko. Associer cela à un polling (analyse complète) est la pratique standard.13

2. La relation entre lettres de lecteur et chemins UNC — Z: appartient à « votre session d’ouverture de session »

Lorsqu’on mappe \\fileserver\share sur Z: dans l’Explorateur, tout donne l’impression qu’un « lecteur Z: » a poussé pour la machine entière. C’est la première méprise.

La documentation officielle est sans ambiguïté sur ce point. Une lettre de lecteur n’est pas globale au système — chaque session d’ouverture de session possède son propre jeu de A à Z. Un lecteur redirigé (un lecteur réseau) ne peut pas être partagé entre des processus s’exécutant sous des comptes utilisateur différents, et un service s’exécutant sous une autre session d’ouverture de session ne peut pas accéder à une lettre de lecteur établie dans une autre session.1 Le système gère les mappages de lecteurs en s’appuyant sur le SID d’ouverture de session qui identifie de façon unique chaque session.1

Autrement dit, Z: n’est rien de plus qu’« un raccourci de chemin, un jeu par session d’ouverture de session » — l’élément sous-jacent est toujours un chemin UNC. À partir de là, toute une série de symptômes fréquemment rencontrés sur le terrain s’explique naturellement.

Symptôme Raison sous-jacente
Z: absent lors d’une exécution sous un autre utilisateur Les lettres de lecteur sont propres à chaque session d’ouverture de session ; le mappage d’un autre utilisateur est invisible1
Z: absent en « Exécuter en tant qu’administrateur » L’UAC crée deux sessions d’ouverture de session, standard et élevée, dont les mappages ne sont pas partagés2
Z: absent une fois mis sur le Planificateur de tâches ou un service Il s’exécute sous une autre session d’ouverture de session. Même si le service est configuré pour s’exécuter sous un compte utilisateur, le système crée quand même une nouvelle session d’ouverture de session pour lui1

Le cas de l’UAC mérite d’être approfondi. Lorsqu’un utilisateur du groupe des administrateurs ouvre une session, le système crée deux sessions d’ouverture de session liées : l’une avec un jeton restreint, l’autre avec le jeton d’administrateur complet. L’élément physique derrière un mappage de lecteur est un objet lien symbolique (DosDevices) associant une lettre de lecteur à un chemin UNC, et cet objet est propre à une session d’ouverture de session donnée — il n’est jamais partagé entre sessions.2 Les scripts d’ouverture de session s’exécutent dans la session à privilèges standard, donc ce mappage est tout simplement invisible depuis un processus élevé — c’est toute l’histoire.

Positionner EnableLinkedConnections (une valeur DWORD sous HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System) à 1 fait écrire le lien symbolique dans les deux sessions liées, ce qui fait disparaître ce symptôme. Cependant, la documentation officielle indique explicitement : « ce contournement peut rendre le système moins sûr. Microsoft ne prend pas en charge ce contournement, qui est fourni tel quel. »3 Implanter un réglage de registre non pris en charge dans l’environnement d’un client est une mauvaise solution pour une application métier. Écrire son application en s’appuyant sur des chemins UNC est la bonne approche. Le guide officiel de dépannage de la redirection de dossiers recommande lui aussi « de toujours utiliser des chemins UNC plutôt qu’une lettre de lecteur mappée ».3

L’implémentation est simple : il suffit de stocker les chemins de la configuration en UNC.

// appsettings.json -- stocker les chemins en UNC, pas en lettres de lecteur
{
  "FileTransfer": {
    "IncomingDir": "\\\\fileserver01\\edi\\incoming",
    "ProcessedDir": "\\\\fileserver01\\edi\\processed"
  }
}

Si votre application permet à un utilisateur de choisir un emplacement sous Z: via une boîte de dialogue de sélection de dossier, le normaliser en UNC au moment de l’enregistrement évite que cela ne casse plus tard, lorsque le contexte d’exécution change. L’API WNetGetUniversalName est justement fournie pour convertir un chemin en lettre de lecteur en forme UNC.14

3. Accéder à un dossier partagé depuis un service Windows — l’UNC est obligatoire, et « en tant que qui » accède-t-on ?

Comme vu au chapitre précédent, un lecteur mappé n’est pas disponible depuis un service. La documentation officielle indique qu’un service (et tout processus s’exécutant dans un contexte de sécurité différent) doit accéder aux ressources distantes par leur nom UNC, et déconseille explicitement de mapper une lettre de lecteur à l’exécution depuis l’intérieur d’un service via net use ou les API WNet. Les raisons avancées sont que le mappage devient visible pour d’autres services s’exécutant dans le même contexte, que les identifiants transmis à net use peuvent fuiter hors des limites du service, et que plusieurs services tentant d’établir le même mappage interfèrent entre eux avec une erreur « déjà connecté ».1

Une fois sur un chemin UNC, la question suivante est l’authentification. En tant que qui le service atteint-il le serveur de fichiers ? Cela est déterminé par le compte d’exécution, qui détermine aussi à qui accorder les permissions côté partage.

Compte d’exécution Identité réseau Attribution de permissions sur le partage Verdict
LocalSystem Les identifiants propres de l’ordinateur4 Dans un domaine, accorder les permissions de partage/NTFS au compte ordinateur (DOMAINE\MACHINE$) Fonctionne, mais sur-privilégié. Remplacer la machine oblige à refaire les permissions
NetworkService Les identifiants propres de l’ordinateur5 Identique à ci-dessus Privilèges locaux minimaux, mais identité réseau identique à LocalSystem
LocalService Identifiants anonymes6 Rien à accorder Inadapté à l’accès à un partage
Compte utilisateur local Ne peut pas atteindre de ressources réseau7
Utilisateur de domaine Ce compte Accorder la permission à ce compte La norme en pratique. La gestion des changements de mot de passe est le coût opérationnel
gMSA Ce compte Accorder la permission à ce compte Le système d’exploitation gère le mot de passe automatiquement (rotation automatique tous les 30 jours). La meilleure option lorsqu’elle est disponible815

Le fait que LocalSystem et NetworkService sortent sur le réseau « en tant que l’ordinateur lui-même » est un fait officiellement documenté.45 La documentation de BITS en tire directement la conséquence pratique : si la liste de contrôle d’accès (ACL) du fichier source restreint l’accès à un compte utilisateur, un service (qui s’authentifie avec les identifiants de l’ordinateur) se verra refuser l’accès, et les comptes système ne devraient pas utiliser de lecteurs mappés.16

Trois lignes directrices pratiques en découlent :

  • Lorsque « en faire un service casse l’accès », la première chose à vérifier est le compte d’exécution. Ce qui fonctionnait sous « vos » permissions sur le poste de travail est désormais vérifié par rapport à l’identité du tableau ci-dessus une fois devenu un service. Vérifiez à la fois les autorisations de partage et l’ACL NTFS pour cette identité.
  • Pour une exploitation à long terme dans un environnement de domaine, exécutez le service sous un compte de domaine ou un gMSA. Un gMSA dispose d’un mot de passe aléatoire de 240 octets que le système d’exploitation fait tourner automatiquement tous les 30 jours, ce qui élimine structurellement la catégorie d’incidents du type « tout s’est arrêté lundi matin parce que le mot de passe du compte de service avait expiré ».15
  • Dans un environnement de groupe de travail (sans domaine), l’authentification par identifiants de l’ordinateur ne s’applique pas, c’est donc là qu’interviennent les identifiants explicites traités au chapitre suivant.

La construction du service lui-même (configuration du compte d’exécution, options de récupération, arrêt sûr) est traitée dans « Comment construire et exploiter un service Windows », et les mécanismes de sessions et d’ouvertures de session sont détaillés dans « Comprendre l’isolation des sessions Windows ».

4. La gestion des identifiants — net use, Gestionnaire d’identifiants et erreur 1219

Lorsque le compte d’exécution lui-même ne peut pas recevoir de permission sur le partage (un groupe de travail, ou un NAS avec son propre système de comptes, par exemple), il faut transmettre explicitement des identifiants lors de la connexion. Il existe principalement trois moyens d’y parvenir :

  • net use \\serveur\partage /user:... – établit la connexion pour cette session d’ouverture de session. Pratique pour une confirmation interactive, mais comme vu précédemment, son exécution depuis l’intérieur d’un service est déconseillée.1
  • Gestionnaire d’identifiants (cmdkey) – enregistrer des identifiants avec cmdkey /add:server /user:svc-file /pass:... fait qu’ils sont utilisés automatiquement pour les authentifications suivantes vers ce serveur.1718 Le piège est que ce stockage est propre à chaque profil utilisateur : si vous devez l’utiliser depuis un service, vous devez l’enregistrer dans le contexte du compte d’exécution du service.
  • Connexion par programme (WNetAddConnection2) – permet d’établir une connexion à une ressource réseau en spécifiant directement les identifiants du client. La documentation officielle cite ceci comme l’une des stratégies qu’un processus serveur peut utiliser pour accéder à des ressources réseau.19 Utiliser une « connexion sans périphérique » (deviceless connection), qui n’attribue aucune lettre de lecteur, permet de continuer à accéder à la ressource purement via son chemin UNC.

Et le piège le plus tristement célèbre de ce domaine est l’erreur 1219.

Multiple connections to a server or shared resource by the same user, using more than one user name, are not allowed. (ERROR_SESSION_CREDENTIAL_CONFLICT, 1219)10

On ne peut pas maintenir plus d’une connexion vers le même serveur, avec des noms d’utilisateur différents, depuis la même session d’ouverture de session. Essayez de vous connecter au même serveur de fichiers avec deux types d’identifiants différents — par exemple votre propre compte pour un partage commercial et un compte dédié pour l’intégration système — et la seconde connexion échoue avec l’erreur 1219. La documentation officielle indique clairement que ceci est voulu (« by design »), et les contournements qu’elle propose sont « se connecter par adresse IP » ou « créer un alias DNS distinct » — autrement dit, faire passer le serveur pour un serveur différent.9

En pratique, le premier choix consiste à éviter d’entrée de jeu une conception utilisant plusieurs identifiants vers un même serveur : consolider sur un seul compte d’intégration et accorder à ce compte les permissions sur tous les partages dont il a besoin. Ce n’est que lorsque cela n’est pas possible qu’il faut séparer les chemins via un alias.

Cela dit, un alias n’est pas aussi simple qu’« ajouter un enregistrement CNAME dans le DNS et c’est terminé ». Dans un environnement Kerberos, un client SMB tente de s’authentifier en utilisant le SPN (nom principal de service) correspondant au nom de la cible, si bien que l’accès à la ressource via un CNAME peut échouer purement et simplement si aucun SPN n’est enregistré pour l’alias. Le guide officiel de dépannage cite justement cette absence de SPN comme l’une des causes, et recommande de configurer l’alias comme un alias de nom d’ordinateur via netdom computername <nom-du-serveur> /add:<alias> plutôt que via un CNAME DNS.20 Avant d’intégrer une solution basée sur un alias dans votre conception comme correctif à l’erreur 1219, vérifiez toujours qu’elle fonctionne réellement dans l’environnement cible.

Une dernière remarque : l’exigence plus avancée consistant à « faire accéder le service au serveur de fichiers avec les permissions de l’utilisateur client qui s’est connecté » relève de l’usurpation d’identité (impersonation), et non de la jonglerie avec des identifiants. La documentation officielle recommande d’ailleurs elle aussi l’usurpation d’identité plutôt qu’un service conservant ses propres identifiants.1 Pour savoir comment implémenter correctement l’usurpation d’identité, et les considérations supplémentaires qui s’appliquent pour les ressources distantes, voir « Bien gérer les jetons d’usurpation d’identité (impersonation) sous Windows ».

5. Concevoir en partant de « lent, coupé, parfois injoignable »

Un code écrit avec le même modèle mental qu’un disque local finira toujours par casser quelque part face à un dossier partagé. Il y a trois réalités à intégrer dès le départ.

Premièrement, la déconnexion des connexions est normale. Une connexion inactive est déconnectée par défaut au bout d’un délai de 15 minutes, pour éviter de gaspiller les ressources du serveur — c’est exactement la fameuse croix rouge que l’on voit sur un lecteur réseau mappé dans l’Explorateur, et le prochain accès la reconnecte rapidement.11 Autrement dit, qu’une application métier n’accédant au partage qu’occasionnellement soit momentanément ralentie à chaque accès, ou qu’un outil de supervision crie « déconnecté ! » sans dommage réel, sont deux comportements parfaitement conformes à la conception du système. Plutôt que de surveiller l’existence de la connexion, il faut juger sur la base de la réussite effective des opérations d’E/S.

Deuxièmement, les erreurs sont désagréablement peu explicites. File.Exists renvoie false — sans lever d’exception — que le chemin soit invalide, que les permissions soient insuffisantes ou qu’il y ait une panne de disque.12 En local, « false signifie que ça n’existe pas » ne pose presque jamais problème ; mais face à un partage, « le fichier n’existe pas » et « injoignable / pas de permission » s’effondrent tous deux dans le même false, si bien que du code qui se branche sur Exists pour prendre une décision métier échoue silencieusement en se comportant comme s’il n’y avait « rien à traiter » lors d’une panne réseau — une façon désagréable de casser. Une conception qui saute la vérification d’existence et ouvre directement le fichier, en distinguant les cas via le type d’exception levée, est plus facile à diagnostiquer face à un partage.

Troisièmement, l’interlocuteur peut ne pas encore être là. Juste après le démarrage d’une machine, le démarrage du service peut précéder la disponibilité du réseau, ou le serveur de fichiers lui-même peut être en cours de redémarrage. Une connexion persistante est un mécanisme restauré à l’ouverture de session de l’utilisateur,21 donc dans le monde d’un service, qui ne passe jamais par une ouverture de session, rien ne garantit que « le partage est visible dès que le service démarre ». Le bon comportement n’est pas de vérifier la connectivité une seule fois au démarrage puis de quitter en cas d’échec — c’est de réessayer en attendant.

En intégrant ces trois éléments, le squelette du côté écriture ressemble à ceci :

// Réessayer les erreurs réseau transitoires ; échouer immédiatement sur les erreurs métier
private static async Task WriteToShareAsync(string finalPath, byte[] content, CancellationToken ct)
{
    var dir = Path.GetDirectoryName(finalPath)!;
    var tempPath = Path.Combine(dir, $"~{Guid.NewGuid():N}.tmp");

    try
    {
        for (var attempt = 1; ; attempt++)
        {
            try
            {
                await File.WriteAllBytesAsync(tempPath, content, ct);
                File.Move(tempPath, finalPath); // Le rename dans le même répertoire publie « l'achèvement »
                return;
            }
            catch (IOException ex) when (attempt < 5 && IsRetryable(ex))
            {
                // Ne réessayer que les défaillances réseau transitoires, avec un backoff exponentiel plafonné
                _logger.LogWarning(ex, "Échec de l'écriture vers le partage (tentative {Attempt}). Nouvelle tentative", attempt);
                await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), ct);
            }
        }
    }
    catch
    {
        // Si on abandonne et qu'on propage l'exception, nettoyer le fichier temporaire au mieux
        try { File.Delete(tempPath); } catch { /* On ignore les échecs de nettoyage */ }
        throw;
    }
}

// IOException arrive avec le même type que ce soit « réseau déconnecté », « un fichier du
// même nom existe déjà à la destination » ou « disque plein ». On utilise les 16 bits de
// poids faible de HResult (le code d'erreur Win32) pour ne sélectionner que les échecs pour
// lesquels une nouvelle tentative a un sens
private static bool IsRetryable(IOException ex)
{
    var win32 = ex.HResult & 0xFFFF;
    return win32 is 53   // ERROR_BAD_NETPATH : le chemin réseau est introuvable
              or 59   // ERROR_UNEXP_NET_ERR : erreur réseau inattendue
              or 64   // ERROR_NETNAME_DELETED : le nom réseau n'est plus disponible
              or 121; // ERROR_SEM_TIMEOUT : le délai d'attente du sémaphore a expiré
}

Il importe aussi de ne pas écrire négligemment « réessayer sur toute IOException ». Des échecs qui produisent le même résultat quel que soit le nombre de tentatives — un fichier du même nom déjà présent à la destination, un chemin trop long, un disque plein — arrivent avec exactement le même type IOException, si bien que juger uniquement sur le type d’exception revient à réessayer cinq fois une erreur permanente et à gaspiller du temps en backoff pour rien. Comme dans l’exemple ci-dessus, filtrez pour ne garder que les « échecs pour lesquels réessayer a effectivement un sens » à l’aide du code d’erreur Win32 (53/59/64/121 et les codes similaires de classe déconnexion/timeout pour l’accès aux partages22). Construire cette liste à partir des codes réellement observés dans les journaux de production est la façon réaliste de l’exploiter.

Le point important n’est pas tant l’ajout de nouvelles tentatives en soi, mais le fait de les associer à un protocole de remise qui reste sûr à réessayer (temp -> rename, idempotence). Si une écriture est interrompue en cours de route, un fichier à moitié terminé reste sur place. Si la promesse est que le nom final n’apparaît que via un rename effectué après fermeture du fichier, on empêche le destinataire de jamais lire un fichier à moitié cuit. Notez également qu’un plantage de processus ou une coupure de courant empêche le code de nettoyage interne à la fonction de s’exécuter, il vaut donc la peine de prévoir aussi un nettoyage périodique du dossier de remise — « supprimer tout fichier ~*.tmp qui n’a pas été touché depuis un certain temps » — pour éviter que les déchets ne s’accumulent et ne finissent par tout engorger. Nous couvrons l’ensemble de cette conception de remise dans « Les bases du verrouillage pour l’intégration de fichiers ».

Il y a encore un piège spécifique au réseau qui mérite d’être signalé. Un résultat « échec » ne garantit pas réellement qu’un échec s’est produit. Si la connexion se coupe juste après que le rename côté serveur s’est achevé, le client peut recevoir une exception alors même que le fichier sous son nom final a déjà été publié. Si l’on réessaie naïvement à ce stade, on se retrouve soit bloqué sur une erreur « un fichier existe déjà à la destination », soit — si le destinataire avait déjà ingéré le premier fichier — on finit par publier deux fois la même donnée. Le remède consiste à vérifier et confronter l’état à la destination avant de réessayer une étape de rename ayant échoué. En intégrant un identifiant de traitement (un numéro de bordereau ou un GUID d’exécution) dans le nom de fichier final, on peut interpréter « un fichier final portant le même identifiant existe déjà » comme signifiant que le rename précédent avait en réalité réussi, et sortir en le traitant comme un succès — et le destinataire peut lui aussi rejeter une ingestion en double sur la base de ce même identifiant.

6. Le verrouillage via SMB et la fiabilité de FileSystemWatcher

6.1. Ne pas trop se fier aux verrous

Un dossier partagé est touché simultanément par plusieurs clients. Ouvrir avec FileShare.None procure bien une exclusivité tant que le descripteur est ouvert, même via SMB, mais une conception qui repose sur « si j’ai obtenu le verrou, c’est sûr » s’effondre lorsqu’un descripteur est perdu à cause d’une déconnexion, ou lorsque quelque chose qui ne prend pas de verrou (une copie manuelle, un autre système) s’invite dans le mélange. La substance réelle de l’exclusion mutuelle doit résider non pas dans un verrou au niveau du système d’exploitation, mais dans le protocole de remise lui-même : temp -> rename, une prise de possession atomique (« claim », où seul le gagnant d’un rename de « incoming » vers « processing » traite le fichier), et l’idempotence. Voir l’article sur le verrouillage cité plus haut pour le détail de cette réflexion.

6.2. La réalité de l’utilisation de FileSystemWatcher sur un partage distant

FileSystemWatcher est officiellement documenté comme prenant en charge la surveillance de fichiers non seulement en local, mais aussi sur des lecteurs réseau et des ordinateurs distants.13 L’utiliser est donc légitime en soi. Il y a cependant deux réserves quant à sa fiabilité.

  • Les notifications arrivent via un tampon, et un débordement les fait perdre. Si les changements arrivent en rafale sur une courte période, les événements au-delà de la taille du tampon sont perdus.13
  • Lors d’une surveillance via le réseau, le plafond de InternalBufferSize est de 64 Ko. Cette contrainte « impossible d’agrandir davantage », par rapport à la surveillance locale, est explicitement documentée.13

J’ai également vu, à de nombreuses reprises sur le terrain de l’investigation de bugs, un symptôme où le watcher meurt silencieusement de l’autre côté lorsque le serveur redémarre ou se déconnecte, sans qu’aucune notification n’arrive pour le signaler. La conclusion pratique est de traiter un FileSystemWatcher sur un partage distant comme un simple indice permettant de réagir plus vite, et de faire d’une analyse complète (polling) au démarrage, en cas d’erreur et de façon périodique la véritable source de vérité. Nous détaillons les schémas de conception pour cela — y compris la gestion des événements dupliqués et désordonnés — dans « Guide pratique de FileSystemWatcher ».

7. Un tableau de décision pratique

Voici un tableau de décision pour les questions de conception récurrentes vues jusqu’ici.

Question Options Recommandation
Comment stocker les chemins Lettre de lecteur / Chemin UNC Tout chemin manipulé par l’application doit être en UNC. Considérez la lettre de lecteur comme une simple commodité visuelle côté utilisateur1
Écriture vers le partage Écriture directe / temp -> rename L’écriture directe n’offre aucune garantie que « le fichier en cours d’écriture ne sera pas lu ». Le nom final doit signifier « complet », en règle générale
Détection des nouveaux fichiers FileSystemWatcher seul / polling / les deux Considérez qu’un partage distant perd des événements. Si un délai de quelques minutes est acceptable, le polling seul est plus simple et plus robuste ; associez les deux si l’instantanéité est nécessaire13
Compte d’exécution du service LocalSystem / compte de domaine / gMSA S’il y a accès à un partage, utilisez un compte de domaine ou un gMSA. Là où un gMSA est disponible, vous pouvez éliminer complètement la gestion des mots de passe8
Lorsque des identifiants distincts sont inévitables Appeler net use depuis le code / WNetAddConnection2 / cmdkey pré-enregistré net use à l’intérieur d’un service est déconseillé. Plusieurs identifiants vers le même serveur se heurtent à l’erreur 1219 ; évitez cela dès la conception en consolidant les comptes ou en utilisant un alias DNS19
Nombreux clients accédant directement Chaque poste accède directement en UNC / via un service intermédiaire (une API) Lorsque le produit du nombre de postes, des identifiants et des accès simultanés devient ingérable, consolidez l’accès au partage dans un seul service et faites dialoguer les clients avec lui via une API

La dernière ligne mérite une remarque. L’intégration via dossier partagé est pratique, mais plus il y a de points d’accès, plus la gestion de qui a quelles permissions et qui entre en conflit avec qui devient exponentiellement difficile. Au-delà d’une certaine échelle, consolidez le processus qui touche le serveur de fichiers dans un seul service Windows, et faites communiquer chaque client avec lui par HTTP ou gRPC. Rétrograder le dossier partagé du statut d’« interface entre systèmes » à celui de « détail d’implémentation interne de ce seul service » est, sur le long terme, la forme la plus facile à maintenir.

8. Résumé

  • Une lettre de lecteur n’est rien de plus qu’un symbole propre à une session d’ouverture de session. Qu’elle soit invisible pour un autre utilisateur, un processus élevé ou un service est voulu par conception — écrivez votre application en vous appuyant sur des chemins UNC. EnableLinkedConnections est un contournement non pris en charge.
  • L’accès à un partage depuis un service exige l’UNC. L’identité d’authentification est déterminée par le compte d’exécution : LocalSystem/NetworkService utilisent les identifiants de l’ordinateur, LocalService est anonyme. En pratique, accordez les permissions côté partage à un compte de domaine ou à un gMSA.
  • Des connexions simultanées vers le même serveur avec plusieurs identifiants échouent avec l’erreur 1219 (voulu par conception). Évitez cela dès la conception via la consolidation des comptes ou un alias DNS.
  • « Lent, coupé, parfois injoignable » est le régime normal d’un partage. Une déconnexion pour inactivité (15 minutes par défaut) n’est pas une anomalie, et un false renvoyé par File.Exists ne peut pas être distingué d’une panne réseau. Intégrez ensemble les nouvelles tentatives, temp -> rename et l’idempotence.
  • FileSystemWatcher prend en charge la surveillance distante, mais un débordement de tampon peut faire perdre des événements et le plafond est de 64 Ko : associer cela à une analyse complète est la pratique standard.
  • En cas de doute, reportez-vous au tableau de décision du chapitre 7 — et si l’échelle grandit, envisagez de consolider l’accès au partage derrière un service intermédiaire.

Articles liés

Domaines de conseil associés

Chez Komura Software LLC, nous prenons en charge la conception et l’implémentation de systèmes d’intégration de fichiers construits autour de dossiers partagés, l’accompagnement sur la configuration des droits d’accès et de l’authentification lors du passage en service Windows, ainsi que l’investigation de bugs tels que « le partage devient invisible dès qu’on en fait un service » ou « ça échoue uniquement dans un environnement spécifique ».

Références

  1. Microsoft Learn, Services and Redirected Drives. Sur le fait que les lettres de lecteur soient attribuées par session d’ouverture de session plutôt que globalement au système, sur l’impossibilité pour les services d’accéder aux lettres de lecteur d’une autre session et la nécessité d’utiliser des noms UNC à la place, sur les raisons pour lesquelles mapper des lecteurs depuis l’intérieur d’un service via net use ou les fonctions WNet est déconseillé (exposition des identifiants, interférence entre services, etc.), sur la création d’une nouvelle session d’ouverture de session même pour un service configuré sous un compte utilisateur, et sur la recommandation de l’usurpation d’identité client plutôt qu’un service conservant ses propres identifiants.  2 3 4 5 6 7 8 9 10 11

  2. Microsoft Learn, Mapped drives are not available from an elevated prompt when UAC is configured to Prompt for credentials. Sur les deux sessions d’ouverture de session liées créées lorsque l’UAC est activé, sur le fait que les mappages de lecteurs reposent sur un objet lien symbolique (DosDevices) propre à une seule session d’ouverture de session et non partagé entre sessions, et sur le fait qu’EnableLinkedConnections force l’écriture du lien symbolique dans les deux sessions.  2 3

  3. Microsoft Learn, Folder Redirection fails to apply when redirected to mapped drive letter, instead of UNC path. Sur le fait que la LSA crée deux jetons d’accès à l’ouverture de session d’un administrateur et que le lecteur est mappé sous le jeton standard, sur les étapes de configuration de la valeur de registre EnableLinkedConnections accompagnées de l’avertissement selon lequel « ce contournement peut rendre le système moins sûr » et n’est pas pris en charge par Microsoft, et sur la recommandation constante d’utiliser des chemins UNC plutôt que des lettres de lecteur.  2 3

  4. Microsoft Learn, LocalSystem Account. Sur le fait que LocalSystem détient d’importants privilèges locaux tout en présentant les identifiants propres de l’ordinateur aux serveurs distants sur le réseau.  2 3

  5. Microsoft Learn, NetworkService Account. Sur le fait que NetworkService détient des privilèges locaux minimaux tout en présentant les identifiants propres de l’ordinateur aux serveurs distants sur le réseau.  2 3

  6. Microsoft Learn, LocalService Account. Sur le fait que LocalService présente des identifiants anonymes sur le réseau.  2

  7. Microsoft Learn, About Service Logon Accounts. Sur le fait que le compte d’ouverture de session d’un service détermine son contexte de sécurité à l’exécution, et sur l’incapacité d’un service s’exécutant dans le contexte de sécurité d’un compte utilisateur local à accéder à des ressources réseau.  2

  8. Microsoft Learn, Group Managed Service Accounts overview. Sur le fait qu’un gMSA est un compte de domaine offrant une gestion automatique des mots de passe et une gestion simplifiée des SPN, permettant de déléguer la gestion des mots de passe au système d’exploitation Windows.  2 3

  9. Microsoft Learn, The network folder specified is currently mapped using a different user name and password error. Sur le fait que l’erreur survenant lors d’une tentative d’établir plusieurs connexions vers le même serveur avec des identifiants différents est voulue par conception, et sur le fait que les contournements sont la connexion par adresse IP ou la création d’un alias DNS distinct.  2 3

  10. Microsoft Learn, System Error Codes (1000-1299). Sur la définition et le texte du message de l’erreur 1219 (ERROR_SESSION_CREDENTIAL_CONFLICT).  2

  11. Microsoft Learn, Mapped drive connection to network share may be lost. Sur le fait qu’une connexion inactive est déconnectée après un délai par défaut de 15 minutes (autodisconnect), et sur le fait que l’icône du lecteur dans l’Explorateur affiche une croix rouge tout en se reconnectant rapidement au prochain accès.  2

  12. Microsoft Learn, File.Exists(String) Method. Sur le fait que la méthode renvoie false sans lever d’exception lorsque la permission de lecture est absente, et renvoie également false si une erreur quelconque survient pendant la vérification de l’existence (chemin invalide, panne de disque, permissions insuffisantes, etc.).  2

  13. Microsoft Learn, FileSystemWatcher Class. Sur la prise en charge de la surveillance de fichiers sur l’ordinateur local, un lecteur réseau et un ordinateur distant, sur la possibilité de perdre des événements en cas de dépassement de la taille du tampon, et sur le fait que la valeur maximale d’InternalBufferSize est de 64 Ko lors d’une surveillance via le réseau.  2 3 4 5

  14. Microsoft Learn, WNet Functions. Sur le fait que WNetGetUniversalName est la fonction qui récupère un nom universel (au format UNC) à partir d’un chemin basé sur une lettre de lecteur. 

  15. Microsoft Learn, Secure group managed service accounts. Sur le fait que le mot de passe d’un gMSA est généré aléatoirement sur 240 octets, automatiquement renouvelé par le système d’exploitation tous les 30 jours, ce qui supprime le besoin pour les administrateurs de planifier des changements de mot de passe ou des interruptions de service.  2

  16. Microsoft Learn, Service Accounts and BITS. Sur le fait que LocalSystem/NetworkService s’authentifient sur le réseau avec les identifiants de l’ordinateur tandis que LocalService utilise des identifiants anonymes, sur le refus d’accès lorsqu’une ACL est restreinte à un compte utilisateur, et sur le fait que les comptes système ne sont pas censés utiliser des lecteurs mappés. 

  17. Microsoft Learn, cmdkey. Sur le fait que la commande cmdkey permet de créer, lister et supprimer des noms d’utilisateur et mots de passe enregistrés (identifiants). 

  18. Microsoft Learn, Credentials processes in Windows authentication. Sur le fait que le Gestionnaire d’identifiants stocke les identifiants dans le conteneur d’identifiants Windows et les présente automatiquement lors des authentifications suivantes. 

  19. Microsoft Learn, Client Access to Network Resources. Sur le fait que l’établissement d’une connexion via WNetAddConnection2 avec des identifiants client spécifiés est cité comme l’une des stratégies qu’un processus serveur peut utiliser pour accéder à des ressources réseau. 

  20. Microsoft Learn, SMB file server share access is unsuccessful through DNS CNAME alias. Sur le fait qu’une absence d’enregistrement SPN pour l’alias est l’une des causes de l’échec de l’accès SMB via un CNAME, et sur la recommandation de définir l’alias via la commande netdom computername plutôt que via un CNAME DNS. 

  21. Microsoft Learn, Windows Networking Operations. Sur le fait qu’une connexion persistante est une connexion réseau que le système restaure automatiquement à l’ouverture de session de l’utilisateur. 

  22. Microsoft Learn, System Error Codes (0-499). Sur les définitions de ERROR_BAD_NETPATH (53), ERROR_UNEXP_NET_ERR (59), ERROR_NETNAME_DELETED (64) et ERROR_SEM_TIMEOUT (121). 

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Pourquoi un service Windows ne peut-il pas accéder à un lecteur réseau mappé (Z:) ?
Parce qu'une lettre de lecteur n'est pas une ressource globale au système : elle appartient à une session d'ouverture de session (logon session) unique. Le lecteur Z: qu'un utilisateur mappe dans l'Explorateur n'est qu'un symbole propre à la session de cet utilisateur, invisible depuis un service exécuté sous une autre session d'ouverture de session. Même lorsqu'un service s'exécute sous un compte utilisateur, le système crée une nouvelle session d'ouverture de session pour ce service, si bien qu'un mappage effectué côté bureau pour ce même compte n'est pas repris. La bonne approche depuis un service consiste à accéder à la ressource via son chemin UNC, \\serveur\partage.
Comment un service exécuté en tant que LocalSystem peut-il accéder à un dossier partagé ?
Sur le réseau, LocalSystem s'authentifie avec les identifiants propres de l'ordinateur. Dans un environnement de domaine, il est possible d'accorder au compte ordinateur de la machine (DOMAINE\MACHINE$) les permissions nécessaires sur le partage (à la fois les autorisations de partage et la liste de contrôle d'accès NTFS) pour que cela fonctionne. Cette configuration est toutefois lourde à exploiter — remplacer la machine oblige à refaire les permissions — c'est pourquoi, en pratique, nous recommandons d'exécuter le service sous un compte de domaine ou un gMSA (compte de service géré de groupe) et d'accorder les permissions du partage à ce compte.
Peut-on surveiller les fichiers d'un dossier partagé avec FileSystemWatcher en toute sécurité ?
On peut l'utiliser, mais il ne faut pas s'y fier seul. FileSystemWatcher prend bien en charge la surveillance de lecteurs réseau et d'ordinateurs distants, mais les notifications de changement arrivent via un tampon interne : si les notifications arrivent en rafale, ce tampon peut déborder et des événements sont alors perdus. Pire, lors d'une surveillance via le réseau, la taille du tampon est plafonnée à 64 Ko. Il faut traiter les notifications comme un simple déclencheur incitant à relancer une analyse, et toujours les associer à une analyse complète (polling) au démarrage, en cas d'erreur, et de façon périodique.
Comment concevoir des échanges de fichiers résilients face aux coupures réseau ?
Il faut concevoir en partant du principe que « le partage est lent, se coupe et peut être injoignable » constitue le cas normal, et non un cas limite. Concrètement : écrire sous un nom de fichier temporaire et le renommer vers son nom définitif seulement après fermeture du descripteur (temp -> rename), envelopper les lectures et écritures dans une logique de nouvelle tentative, et distinguer les erreurs réseau transitoires des véritables erreurs métier. Notez que File.Exists renvoie false même lorsque le fichier est inaccessible, et qu'il ne permet donc pas de distinguer « le fichier n'existe pas » de « le serveur est injoignable » — et, en dernier recours, veillez à ce que le traitement reste idempotent afin qu'un retraitement ne provoque jamais de corruption.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog