You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

自托管.NET Kestrel Web服务器锁定字体文件,卸载/重装需重启Windows

.NET Kestrel静态服务字体文件锁定问题解决方案

1. 锁定产生的原因

出现这个问题核心有两点:

  • Windows字体引擎的保护机制:浏览器渲染Web字体时,会调用Windows的DirectWrite/GDI字体渲染引擎加载文件。为防止字体在渲染过程中被篡改,系统会对文件施加排他锁,直到浏览器进程关闭或系统字体缓存服务(FontCache3.0.0.0)释放句柄。
  • Kestrel默认文件读取限制:Kestrel静态文件中间件默认以FileShare.Read模式打开文件,这种模式下文件仅允许读取,无法被删除或修改——如果Kestrel还持有文件句柄,卸载时就会触发锁定。

2. 如何避免文件锁定

调整Kestrel的文件读取模式

自定义静态文件提供程序,针对字体文件使用允许读写、删除的共享模式打开,确保即使文件正在被读取,也能被卸载程序删除。
代码实现示例:

// 自定义文件提供程序,处理字体文件的解锁读取
public class UnlockedFontFileProvider : PhysicalFileProvider
{
    private readonly string[] _fontExtensions = { ".woff", ".woff2", ".ttf", ".otf", ".eot" };

    public UnlockedFontFileProvider(string root) : base(root) { }

    protected override FileInfo GetFileInfo(string subpath)
    {
        var fileInfo = base.GetFileInfo(subpath);
        if (fileInfo.Exists && IsFontFile(fileInfo.Name))
        {
            return new UnlockedFontFileInfo(fileInfo);
        }
        return fileInfo;
    }

    private bool IsFontFile(string fileName)
    {
        var ext = Path.GetExtension(fileName).ToLowerInvariant();
        return _fontExtensions.Contains(ext);
    }
}

public class UnlockedFontFileInfo : IFileInfo
{
    private readonly FileInfo _innerFile;

    public UnlockedFontFileInfo(FileInfo innerFile) => _innerFile = innerFile;

    // 实现IFileInfo基础属性
    public bool Exists => _innerFile.Exists;
    public long Length => _innerFile.Length;
    public string PhysicalPath => _innerFile.FullName;
    public string Name => _innerFile.Name;
    public DateTimeOffset LastModified => _innerFile.LastWriteTimeUtc;
    public bool IsDirectory => _innerFile.Attributes.HasFlag(FileAttributes.Directory);

    // 关键:使用允许读写和删除的共享模式创建流
    public Stream CreateReadStream()
    {
        return new FileStream(
            PhysicalPath,
            FileMode.Open,
            FileAccess.Read,
            FileShare.ReadWrite | FileShare.Delete
        );
    }
}

在Program.cs中替换默认静态文件提供程序:

var app = builder.Build();
var appRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "MyApp");

app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new UnlockedFontFileProvider(appRoot)
});

卸载前清理相关进程

在卸载脚本中,先停止Kestrel服务,再强制关闭所有可能加载了该字体的浏览器进程(如Chrome、Edge等)。但这种方法会中断用户浏览器会话,体验较差,仅作为备选。

3. 更优解决方案

将字体打包为嵌入式资源

把字体文件设为项目的嵌入式资源,通过自定义中间件从程序集中读取资源,完全脱离磁盘文件依赖,从根源上避免锁定问题。
代码示例:

// 映射字体请求路由
app.Map("/fonts", fontApp =>
{
    fontApp.Run(async context =>
    {
        var fontName = context.Request.Path.Value.TrimStart('/');
        var assembly = System.Reflection.Assembly.GetExecutingAssembly();
        // 资源名称格式为「命名空间.文件夹.文件名」,需根据项目结构调整
        var resourcePath = $"MyApp.Resources.Fonts.{fontName}";

        using var resourceStream = assembly.GetManifestResourceStream(resourcePath);
        if (resourceStream == null)
        {
            context.Response.StatusCode = StatusCodes.Status404NotFound;
            return;
        }

        // 设置对应字体的Content-Type
        var contentType = fontName switch
        {
            string f when f.EndsWith(".woff2") => "font/woff2",
            string f when f.EndsWith(".woff") => "font/woff",
            string f when f.EndsWith(".ttf") => "font/ttf",
            string f when f.EndsWith(".otf") => "font/otf",
            string f when f.EndsWith(".eot") => "application/vnd.ms-fontobject",
            _ => "application/octet-stream"
        };
        context.Response.ContentType = contentType;
        await resourceStream.CopyToAsync(context.Response.Body);
    });
});

使用CDN托管Web字体

如果条件允许,将Web字体上传到公共或私有CDN,HTML直接引用CDN地址。这种方式不需要本地存储字体文件,彻底规避了本地文件锁定问题,还能提升资源加载速度。

内容的提问来源于stack exchange,提问作者jtalarico

火山引擎 最新活动