You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Windows Service中使用WebClient下载文件失败问题排查

问题排查与修复方案

你的Windows服务无法正常执行文件下载,核心问题集中在服务运行环境差异、错误被隐藏以及配置/权限冲突上,以下是具体排查点和修复方式:

1. 环境目录错误:Environment.CurrentDirectory不指向程序安装路径

Windows服务默认工作目录是C:\Windows\System32,而非你的服务程序所在文件夹。这会导致两个问题:

  • 找不到config.ini文件,因为你用Environment.CurrentDirectory拼接路径,实际配置文件在服务安装目录;
  • 若ConfigPath读取失败,下载文件会默认写入System32,普通服务用户(如Network Service)无写入权限。

修复:
替换所有Environment.CurrentDirectory,改用程序集路径获取安装目录:

// 获取服务程序的实际安装路径
string appPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
// 用appPath替换原有的Environment.CurrentDirectory
string sPath = Path.Combine(appPath, "config.ini");

2. 空catch块吞掉所有错误

你所有的catch (Exception ex)都没有任何处理逻辑,就算代码抛出异常(比如文件找不到、权限不足、网络错误),也无法感知到问题。这就是日志无内容的核心原因——错误被完全隐藏了。

修复:
在catch块中添加可靠的日志记录,比如写入Windows事件日志(需确保服务用户有创建事件源的权限):

catch (Exception ex)
{
    const string sourceName = "MyDownloadService";
    if (!EventLog.SourceExists(sourceName))
        EventLog.CreateEventSource(sourceName, "Application");
    EventLog.WriteEntry(sourceName, $"下载失败:{ex.Message}\n{ex.StackTrace}", EventLogEntryType.Error);
}

3. WebClient认证冲突

你同时设置了Basic Auth头和UseDefaultCredentials = true,这两个配置会互相冲突:UseDefaultCredentials会自动添加当前用户的Windows凭证,覆盖你手动设置的Basic Auth头,导致认证失败。

修复:
删除client.UseDefaultCredentials = true;这一行,仅保留Basic Auth配置:

try {
    client.Headers.Add("Authorization", String.Format("Basic {0}", basicToken));
    // 移除该行:client.UseDefaultCredentials = true;
    client.DownloadFile(url, Path.Combine(this.ConfigPath, PricelistFilename));
} catch (Exception ex) {
    // 这里添加日志记录
}

4. 目标目录权限不足

就算ConfigPath读取正确,服务运行的用户(如Network Service、Local Service)可能没有目标目录的写入权限。比如ConfigPath设为C:\Users\XXX\Downloads时,Network Service默认无访问权限。

修复:

  • 选择服务用户有写入权限的目录,比如C:\ProgramData\YourServiceName;
  • 手动给目标目录添加服务运行用户的写入权限;
  • 代码中可自动创建默认目录(需确保父目录有权限):
    if (!Directory.Exists(this.ConfigPath)) {
        Directory.CreateDirectory(this.ConfigPath);
    }
    

5. 定时器初始化顺序优化

你先读取配置再绑定定时器事件,虽然不影响功能,但建议调整顺序,先绑定事件再读取配置(避免配置修改Interval后事件未生效的潜在问题):

protected override void OnStart(string[] args) {
    this.User = "USER";
    this.Pass = "PASS";
    // 先绑定定时器事件
    LookupTimer.Elapsed += LookupTimer_Elapsed;
    // 再读取配置(可能修改Interval)
    this.ReadFromConfigFile();
    LookupTimer.Enabled = true;
}

完整修复后的关键代码片段

System.Timers.Timer LookupTimer = new System.Timers.Timer();
string ConfigPath = "";
string PricelistFilename = "";
string User = "";
string Pass = "";
// 程序安装路径
private string AppPath => Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);

protected override void OnStart(string[] args) {
    this.User = "USER";
    this.Pass = "PASS";
    LookupTimer.Elapsed += LookupTimer_Elapsed;
    this.ReadFromConfigFile();
    LookupTimer.Enabled = true;
    LogEvent("服务已启动");
}

private void LookupTimer_Elapsed(object sender, ElapsedEventArgs e) {
    string url = "https://mywebsite.com/file.csv";
    WebClient client = new WebClient();
    string basicToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{this.User}:{this.Pass}"));
    try {
        client.Headers.Add("Authorization", $"Basic {basicToken}");
        string savePath = Path.Combine(this.ConfigPath, PricelistFilename);
        client.DownloadFile(url, savePath);
        LogEvent($"文件成功下载至:{savePath}");
    } catch (Exception ex) {
        LogEvent($"下载失败:{ex.Message}\n{ex.StackTrace}", EventLogEntryType.Error);
    }
}

public void ReadFromConfigFile() {
    StreamReader loReader = null;
    string sPath = Path.Combine(AppPath, "config.ini");
    string sInterval = "0";
    try {
        if (File.Exists(sPath)) {
            loReader = new StreamReader(sPath, Encoding.UTF8);
            string lsLine = "";
            while (!loReader.EndOfStream) {
                lsLine = loReader.ReadLine();
                string[] lines = lsLine.Split('=');
                if (lines.Length == 2) {
                    string keyname = lines[0].Trim();
                    string valuename = lines[1].Trim();
                    if (keyname.ToUpper() == "FOLDER") {
                        this.ConfigPath = valuename;
                    } else if (keyname.ToUpper() == "DURATION") {
                        sInterval = valuename;
                        if (!string.IsNullOrWhiteSpace(sInterval)) {
                            try {
                                LookupTimer.Interval = Convert.ToInt32(sInterval);
                            } catch (Exception ex) {
                                LogEvent($"解析间隔出错:{ex.Message}", EventLogEntryType.Warning);
                                sInterval = "0";
                            }
                        }
                    } else if (keyname.ToUpper() == "FILENAME") {
                        this.PricelistFilename = valuename;
                    }
                }
            }
        } else {
            LogEvent($"未找到配置文件:{sPath}", EventLogEntryType.Warning);
        }
    } catch (Exception ex) {
        LogEvent($"读取配置出错:{ex.Message}", EventLogEntryType.Error);
    } finally {
        loReader?.Close();
    }

    // 处理默认路径
    if (string.IsNullOrWhiteSpace(this.ConfigPath) || !Directory.Exists(this.ConfigPath)) {
        this.ConfigPath = Path.Combine(AppPath, "Downloads");
        if (!Directory.Exists(this.ConfigPath)) Directory.CreateDirectory(this.ConfigPath);
    }
    if (string.IsNullOrWhiteSpace(sInterval)) {
        this.LookupTimer.Interval = 3600000; // 默认1小时
    }
    if (string.IsNullOrWhiteSpace(PricelistFilename) || !PricelistFilename.ToUpper().EndsWith(".CSV")) {
        PricelistFilename = "FILE.csv";
    }
}

// 封装日志方法
private void LogEvent(string message, EventLogEntryType type = EventLogEntryType.Information) {
    const string sourceName = "MyDownloadService";
    try {
        if (!EventLog.SourceExists(sourceName)) {
            EventLog.CreateEventSource(sourceName, "Application");
        }
        EventLog.WriteEntry(sourceName, message, type);
    } catch {
        // 防止日志本身出错导致服务崩溃
    }
}

额外测试建议

  • 先手动运行服务的EXE文件(在控制台窗口),直接查看控制台输出,更容易排查初始化和下载错误;
  • 测试时可临时将服务设置为管理员账户运行,验证是否为权限问题;
  • 检查目标下载目录是否存在,以及服务用户是否有写入权限。

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 04:19:58