云计算百科
云计算领域专业知识百科平台

从零搭建半导体设备上位机(四):单实例、启动流程与配置文件,App 里那 526 行到底在干什么

单实例、启动流程与配置文件:App 里那 526 行到底在干什么

这是「从零搭建半导体设备上位机」系列第4篇。上一篇讲了两套 MVVM 框架怎么分。这一篇拆开那个 526 行的 App.xaml.cs:启动时到底按什么顺序做事、两套配置文件各管什么、以及几个"看起来没事其实有坑"的写法。


打开这个文件之前

上一篇我说过,RegisterTypes 有两百行。那剩下的三百多行在干什么?

答案是:启动流程。

工控上位机的启动比普通桌面软件麻烦得多。普通软件启动失败,用户关掉重开就行。设备上的上位机启动失败,操作员站在机器前,后面排着生产计划,你没法跟他说"重启试试"——启动阶段必须把能想到的失败都兜住,并且给出人话的错误。

这 526 行里,真正跟界面有关的不到 50 行,剩下全是"把设备连上、把配置读对、把日志开起来"。


一、OnStartup 的真实顺序

先看代码里实际的执行顺序(不是教科书上的理想顺序):

顺序做什么失败会怎样
1 决定日志目录 建目录失败 → 整个启动中断
2 读"开发标记"文件 无(只是设置一个全局标志)
3 读 AppConfig(JSON) 解析失败 → 弹错误框后退出
4 读 / 补 Config.ini 文件缺失 → 从安装目录复制一份默认的
5 决定 ADS 的 NetID(本地 / 远程) NetID 为空 → 弹配置确认框
6 创建单实例 Mutex 已有实例 → 提示后退出
7 注册全局异常处理 —
8 进入 Prism 的 CreateShell —

整个流程包在一个 try/catch 里,catch 里做两件事:写日志、弹框、清理资源、Shutdown()。

1.1 日志目录不是固定的

var logRoot = Path.Combine(Golbal.WorkRootPath + @"\\Logs");
if (File.Exists(@"D:\\App\\RoboCell\\LogNet.tag"))
logRoot = Path.Combine(Golbal.LotRootNetPath + @"\\Logs");

第一行是默认:日志写在软件自己的工作目录下。
第二行是例外:如果存在某个标记文件,日志改写到网络路径。

为什么要有这个?因为现场设备装在无尘室里,工程师不能随便进去。设备出问题的时候,要么穿无尘服进去拷日志,要么——让软件自己把日志写到网络盘上,在外面看。

一个标记文件就切换了日志落点,成本极低,收益极大。这个设计后来被复用到了好几个地方。

1.2 "开发标记"文件:用文件系统当配置开关

Golbal.IsDevDebug = File.Exists(Path.Combine(@"D:\\App\\RoboCell", "dev.flag"));

整个应用里,凡是"开发时想要、生产时不要"的行为,都挂在这个布尔值上:

  • 默认启动页面不同(开发进调试界面,生产进正式界面)
  • 部分调试按钮显隐
  • 日志级别

为什么用文件而不是配置文件里的一个字段?

因为改配置文件要重启软件,而现场工程师不一定知道配置文件在哪。放一个空文件在固定路径,谁都能做:要开调试就建文件,要关就删掉。

代价是这个行为完全不可见——你在配置文件里找不到它,只能读代码才知道。这就是第 26 篇要讲的"调试标记系统"的由来,也是它后来必须被收口的原因。

1.3 配置文件里的"初始页面"永远不生效

这段代码值得单独看:

var programDefaultView = CurAppConfig.DefaultView; // 先存代码里的默认值
if (!string.IsNullOrWhiteSpace(configStr))
{
CurAppConfig = JsonConvert.DeserializeObject<AppConfig>(configStr)!;
CurAppConfig.DefaultView = programDefaultView; // 反序列化完再覆盖回去
}

意思是:JSON 里存的 DefaultView 被丢弃,永远用代码里算出来的那个。

为什么?因为默认页面依赖 IsDevDebug:

public string DefaultView { get; set; } =
Golbal.IsDevDebug ? ProcessView.ViewName : DesignView.ViewName;

开发模式和生产模式要进不同页面。如果允许配置文件覆盖,就出现"生产环境读了开发环境的配置,开机进调试界面"这种事——现场是要报故障的。

所以设计上直接堵死:这个字段只是"能被序列化出去给人看",不能被读回来生效。

代价是它很反直觉。后来我在字段注释里写清楚了,但注释这种东西,改代码的人不一定看。


二、两套配置文件,分工是明确的

文件格式管什么谁改
Configs/AppConfig.json JSON 窗口宽高、是否最大化、初始页面 软件自己写回
Config.ini INI PLC 连接(本地/远程、NetID、端口)、数据库访问模式、传感器读取延迟、是否开机自启 现场工程师手改

两套配置文件的分工

为什么不用一套

因为改动频率和责任人不一样。

窗口大小是"用户拖一下就变",软件自己记;PLC 的 NetID 是"装机时配一次,之后只在换设备时改",必须是人手改,而且必须是纯文本、能拿记事本打开。

用 JSON 存 NetID,现场工程师改错一个逗号,整个文件解析失败,软件起不来。INI 容错性好得多——写错了顶多某个键读不到。

工控软件的配置文件格式选择,第一原则是"人手改不出致命错误",不是"结构优雅"。

数据库访问模式:一个枚举管四种环境

Config.ini 里有个字段控制数据库连哪:

值模式说明
0 Dev 本地开发库
1 Test 测试库
2 Product_In 生产模式(无尘室内部网络)
3 Product_Out 生产模式(外部网络)

同一个软件,四种数据库落点。这不是设计得漂亮,是被网络环境逼出来的:无尘室内的网络和外面的网络不通,设备在里面跑,数据要同时给里面和外面看。

切换靠改一个数字。好处是简单;坏处是改错了没人知道——连到测试库,生产数据写进了测试表,界面上一切正常。

后来加了一条:启动时把当前数据库模式打进日志第一行,并且显示在状态栏角落。让"连的是哪个库"这件事始终可见。


三、Mutex 放在最后,是个真实隐患

单实例锁是这么写的:

IMutex = new Mutex(true, "RoboCell_Run_App");
if (IMutex.WaitOne(0, false))
{
base.OnStartup(e);
DispatcherUnhandledException += App_DispatcherUnhandledException;
}
else
{
MessageBox.Show("程序已经启动,只能运行一个实例", "提示", ...);
Shutdown();
}

问题在于:它在配置读取之后。

也就是说,第二个实例启动时,会先把日志目录建一遍、读一遍配置文件、初始化一遍 ADS 参数,然后才发现"已经有实例在跑了"。

后果有三个,都不致命但很烦:

  • 第二个实例会往同一个日志文件里写几行(日志框架允许共享写,但内容会交错)。
  • 如果配置文件损坏,第二个实例会先弹"配置读取失败",再弹"已经启动",两个框叠在一起。
  • 启动慢——读了配置才退出,白白多做几百毫秒。
  • 正确顺序是把 Mutex 提到最前面,进 OnStartup 第一件事就是抢锁,抢不到直接退出。

    当时没这么改,是因为"先读配置"这个顺序已经跑了两年没出过事——没出过事不等于没问题,只是没被触发过。


    踩坑记录

    坑一:配置文件损坏 = 软件起不来

    catch 里是这么处理的:

    catch (Exception ex)
    {
    AppLog.Error("读取App配置文件失败,请联系开发人员!", ex);
    MessageBox.Show($"读取App配置文件失败…\\r\\n错误信息:{ex.Message}", "错误", ...);
    CleanupResources();
    Shutdown();
    }

    合理,但不够。

    真实情况是:配置文件损坏之后,每次启动都弹同一个框然后退出,操作员没有任何自救手段——他不知道文件在哪、也不知道该删哪个。

    后来改进成:配置文件解析失败时,自动备份坏文件并回退到默认配置,同时在界面上用红色横幅提示"配置已重置,请检查 PLC 连接参数"。

    原则是:能让软件起来就先起来,起来之后再用显眼的方式告诉人哪里不对。 一个连界面都进不去的软件,现场只能叫工程师。

    坑二:本地 PLC 和远程 PLC 走的是两条路

    if (AppIniConfig.IsLocalPlc)
    _netID = AmsNetId.Local.ToString();
    else
    _netID = AppIniConfig.PlcNetId;

    本地模式下 NetID 用本机 AMS 地址,远程模式用配置里的。

    这条分支看着简单,实际踩过:开发机上 IsLocalPlc=true,装机时忘了改,软件能连上(因为开发机上也装了 TwinCAT 运行时),但连的是本机的虚拟 PLC,不是设备上的真 PLC。

    现象是:界面数据全在动,但都是假的。查了两小时才反应过来。

    后来加了一条启动日志:连接成功之后,把实际的 NetID 和端口打印出来,并且和配置文件里的值做一次比对,不一致就告警。

    坑三:C:\\ 下的路径

    早期所有路径都硬编码在 C:\\ 下(日志、配置、标记文件)。装机时遇到两个问题:

  • 部分设备的系统盘开了还原,重启后 C:\\ 下的配置全没了。
  • 没有管理员权限时,往 C:\\ 根目录写文件会被拦。
  • 现在统一挪到了软件目录下(Golbal.WorkRootPath),只有标记文件还留在固定路径——因为它需要在软件目录之外,否则重装软件会把它一起删掉。

    “标记文件要独立于软件目录” 这个原则,是从"重装软件把调试开关一起删了、结果生产环境进了调试界面"那次事故之后定下的。


    本篇小结

  • 启动流程的顺序是有讲究的:日志先起、配置后读、锁要最先抢。
  • 两套配置文件按"谁改、改多频繁"分:JSON 给软件写,INI 给人改,因为人手改不出致命错误更重要。
  • 文件系统当开关(标记文件)成本低、可见性差,用完必须有收口机制。
  • 配置文件损坏时的正确做法是降级启动,不是弹框退出——现场没人能改代码。
  • 下一篇进界面层。三套 UI 库(HandyControl、MaterialDesign、Wu.Wpf)同时装在一个 WPF 应用里,样式到底怎么不打架。


    下期预告

    第5篇:三套 UI 库共存:HandyControl、MaterialDesign、Wu.Wpf 的样式基线

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 从零搭建半导体设备上位机(四):单实例、启动流程与配置文件,App 里那 526 行到底在干什么
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!