我在厂里搞wine的日子

之前工作中搞过一段时间的wine,主要是解决一些第三方应用的安装或运行问题,后面好长时间没搞了,有次电脑出问题重装系统的时候整理文档,发现之前还写过一些日志,于是找时间把日志粗略整理了一下,分享出来供大家批评参考。

InkRecognizer不工作(COM组件/注册表缺失问题)

问题描述

希沃白板的汉字识别卡,正常状态下,在书写时左侧会实时更新待选汉字,类似于手写输入法;异常状态下,左侧的待选汉字列表总是为空。根据希沃白板的汉字识别卡的原理写了一个简单的Demo用于调试:

复制代码
using Microsoft.Ink

private void ButtonClick(object sender, RoutedEventArgs e)
{
    Console.WriteLine("button click");
    try
    {
        using (MemoryStream ms = new MemoryStream())
        {
            Console.WriteLine("new ms");
            theInkCanvas.Strokes.Save(ms);
            Console.WriteLine("Save strokes into ms");
            var myInkCollector = new InkCollector();
            var ink = new Ink();
            ink.Load(ms.ToArray());
            Console.WriteLine("Load strokes from ms to ink");

            using (RecognizerContext context = new RecognizerContext())
            {
                //...
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"{ex.HResult:X} : {ex.Message}");
    }
}

把Demo放到Wine上运行,得到相关的wine的日志输出:

复制代码
button click
new ms
Save strokes into ms

//先尝试创建进程内COM对象
0024:err:ole:com_get_class_object class {43fb1553-ad74-4ee8-88e4-3e6daac915db} not registered

//再尝试从本地机器创建进程外COM对象
0024:err:ole:create_server class {43fb1553-ad74-4ee8-88e4-3e6daac915db} not registered

//最后尝试从远程机器创建进程外COM对象(没实现,所以是fixme)
0024:fixme:ole:com_get_class_object CLSCTX_REMOTE_SERVER not supported

//三种方式都不行,COM对象无法创建
0024:err:ole:com_get_class_object no class object {43fb1553-ad74-4ee8-88e4-3e6daac915db} could be created for context 0x15
80040154 :

最初的思路

从日志来看,问题是缺少dll或者dll未注册导致的,0x80040154的错误码表示的是COM类型未注册(一些内部的错误代码,不知道其代表什么意思的话,可以在相关模块的wine的源码里进行搜索,比如这个0x80040154搜索到的是REGDB_E_CLASSNOTREG,然后查找REGDB_E_CLASSNOTREG的引用,结合日志,就能够知道返回错误代码的大致的原因),从错误代码返回的路径来看,是ole模块加载COM组件时,无法从注册表查找到{43fb1553-ad74-4ee8-88e4-3e6daac915db},因此最初的解决思路是把缺少的注册表从Windows上拷贝过去。

复制代码
[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{43FB1553-AD74-4ee8-88E4-3E6DAAC915DB}]
@="InkCollector Class"

[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{43FB1553-AD74-4ee8-88E4-3E6DAAC915DB}\InprocServer32]
@="%CommonProgramFiles%\Microsoft Shared\Ink\InkObj.dll"
"ThreadingModel"="Both"

[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{43FB1553-AD74-4ee8-88E4-3E6DAAC915DB}\ProgID]
@="msinkaut.InkCollector.1"

[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{43FB1553-AD74-4ee8-88E4-3E6DAAC915DB}\Programmable]

[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{43FB1553-AD74-4ee8-88E4-3E6DAAC915DB}\VersionIndependentProgID]
@="msinkaut.InkCollector"

把这些注册表拷过去后,wine的日志输出如下:

复制代码
button click
new ms
Save strokes into ms
0024:err:ole:com_get_class_object class {937c1a34-151d-4610-9ca6-a8cc9bdb5d83} not registered
0024:err:ole:create_server class {937c1a34-151d-4610-9ca6-a8cc9bdb5d83} not registered
0024:fixme:ole:com_get_class_object CLSCTX_REMOTE_SERVER not supported
0024:err:ole:com_get_class_object no class object {937c1a34-151d-4610-9ca6-a8cc9bdb5d83} could be created for context 0x15
80040154 :

日志和前面相似,只是未注册的class不一样了,class对应的注册结构和前面也一样的:

复制代码
[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{937C1A34-151D-4610-9CA6-A8CC9BDB5D83}]
@="InkObject Class"

[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{937C1A34-151D-4610-9CA6-A8CC9BDB5D83}\InprocServer32]
@="%CommonProgramFiles%\Microsoft Shared\Ink\InkObj.dll"
"ThreadingModel"="Both"

[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{937C1A34-151D-4610-9CA6-A8CC9BDB5D83}\ProgID]
@="msinkaut.InkObject.1"

[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{937C1A34-151D-4610-9CA6-A8CC9BDB5D83}\Programmable]

[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{937C1A34-151D-4610-9CA6-A8CC9BDB5D83}\VersionIndependentProgID]
@="msinkaut.InkObject"

和前面一样,把注册表导入进去,再运行应用,结果还是类似的日志,只是未注册的class又不一样了。

二战转折点

导入了注册表和dll后,又提示缺少其他的东西,重复了很多次之后,还是不能正常运行,一度怀疑思路不对。直到有人提出把Windows上的注册表全部导入到wine里面试试,试了一下之后,能够正常运行了。这样看来基本思路是正确的,只是缺少的注册表和dll的数量可能比预想的多得多。

从前面缺少的注册表来看,它们都存在一个InprocServer32键,值为"%CommonProgramFiles%\Microsoft Shared\Ink\InkObj.dll",因此,我用Registry Finder搜索"%CommonProgramFiles%\Microsoft Shared\Ink\InkObj.dll"得到的所有注册表都导入进去了。结果还是有类似的报错,但是报错的注册表项指向的dll不再是InkObj.dll了。

于是扩大搜索范围,改为搜索"Ink",将结果全部导入后,提示缺少Recognizer,于是搜索"Recognizer"。这个比较难找,因为搜索结果看起来都跟Ink没啥关联,只能一个个尝试,最后在导入\Microsoft\TPG\Recognizers这个节点后成功运行起来了。

总结

回过头来看的话这其实是一个COM组件缺少问题,在没有其他方式注册COM组件的前提下,也只能手动拷贝注册表。只是一开始没有预估到光一个手写识别的COM组件涉及到的注册表就有一百多项(从COM组件的原理上来讲,通常都是会很多的,因为每一个COM类型、COM接口都对应一个注册表项,而且都是分散的),所以提示缺什么补什么的方式搞了很久都没搞定。

其实,我们的Demo中引用的Microsoft.Ink.dll本身并不是COM组件,而是对COM组件的C#封装。我们在WPF中使用COM组件时,都有可能会用到类似的dll,例如DirectShowLib、Microsoft.Office.Interop.*等等,这些都是对quartz.dll、office.dll这些真正的COM组件的封装。COM组件的C#封装里,有许多类似这样的接口申明和类申明:

复制代码
[ComImport]
 [TypeLibType(4096)]
 [InterfaceType(2)]
 [Guid("11A583F2-712D-4FEA-ABCF-AB4AF38EA06B")]
 internal interface _IInkCollectorEvents
 {
     //...
 }
 
 [ComImport]
 [ClassInterface(0)]
 [ComSourceInterfaces("Microsoft.Ink._IInkCollectorEvents")]
 [SuppressUnmanagedCodeSecurity]
 [Guid("43FB1553-AD74-4EE8-88E4-3E6DAAC915DB")]
 [TypeLibType(2)]
 internal class InkCollectorClass : IInkCollector, InkCollectorPrivate, _IInkCollectorEvents_Event
 {
     //...
 }

要补充COM组件缺失的注册表,其实主要是需要把上面这些Interface和Class的Guid相关的注册表项导入进去就可以了,另外,Class的注册表通过会有一个路径指向一个dll,表示这个Class应当从这个dll里进行实例化,因此这个dll也要考到相应路径。

最后呢,COM组件可能本身会依赖一些其他的组件,比如书写识别需要Recognizer,DirectShow需要Filter。这些依赖一般不会特别多,而且一般不会特定地依赖某一个组件,而是有合适的就可以,所以这些缺少的依赖根据错误提示进行处理问题不会太大。

中望CAD无法安装(应用安装问题)

中望CAD的安装问题不算特别复杂,简单说一下。中望CAD下载的是官网最新的2023版本,一开始安装不了,安装器提示windowscodecs安装不了。

于是在winetricks里查找windowscodecs进行安装,然后再重新尝试安装中望CAD,这次安装成功了,但是无法运行。

有了通过winetricks安装windowscodecs这一步的经验,想到可能还有其他依赖没有成功安装,于是把安装包解压了,在一个名字为dependencies的文件夹里看到了很多安装包。尝试手动安装这些安装包,无法安装的再尝试通过winetricks进行安装。把能安装的都安装完成后,中望CAD安装成功,正常启动。

中望CAD的安装过程可以总结一个流程:

  1. 解压应用安装包,找到安装包内附带的依赖包
  2. 手动安装依赖包,或通过winetricks安装
  3. 重新运行应用安装包

部分无法安装的应用可能能够通过这个流程进行安装,但是这个流程能够解决的安装问题非常有限,下列这类问题都是无法解决的:

  1. 本身安装流程存在其他问题
  2. 依赖包无法安装,winetricks上也没有可用的安装包

对于第2点,可以把问题转为依赖包的安装问题。

希沃白板视频无法播放(代码问题)

希沃白板内有策略可以选择MediaElement或者MediaUriElement作为视频播放器。MediaElement是框架自带的控件,调用的是Windows Media Player组件,在wine上可以弹窗播放;MediaUriElement是希沃写的控件,基于DirectShow解码和D3DImage渲染,在wine上无法播放。

一开始的思路是,对比MediaElement和MediaUriElement在wine上运行的日志,寻找可疑的点。这个思路基于一个认知:MediaUriElement的实现应该大致上和MediaElement是差不多的,可能是希沃参照MediaElement写的一个控件。根据这个思路,也找到了一些可疑的日志,但验证后都被排除了。

随着调试的深入,感觉到MediaUriElement的实现和MediaElement的实现有很大不同,这意味着,通过对比日志可能很难定位到异常,因为日志的差异里面可能有很大一部分是跟问题无关的。

在验证一些日志差异的时候,也用到dnSpy对希沃反编译的源码进行调试。原本能够在源码级别进行调试的话,给我的感觉是问题应该能够很快定位到的,但实际操作中才发现存在一个很大的困难:希沃的代码很复杂,调用堆栈很深,再加上视频播放的问题并没有异常中断,因此即使有源码的加持,也很难确定问题所在。

这个困难可能也是我们以后会经常遇到的一个困难,因为我们遇到的很多问题都是wine上的表现和windows上不一致的问题,而不是能够抛出异常或者打印出错误日志的问题,这导致即使我们拥有源码,能够进行代码调试,调试器也不会在某一行异常代码处中断,在不了解实现逻辑,不熟悉代码结构的情况下,仍然很难定位到具体的代码。

因为对比日志这条思路走不下去了,所以决定换个思路,准备按照希沃的实现自己写一个MediaUriElement进行调试,这对定位问题有两个可能的好处:

  1. 写完代码之后对MediaUriElement的实现会有个大致的理解
  2. 在MediaUriElement中可以插入一些日志,结合wine的日志,能够把WPF上的执行节点和wine上的执行节点对应起来

简化的MediaUriElement的类图如上图所示,MediaUriElement继承自D3DRenderer,D3DRenderer内是使用D3DImage进行视频帧渲染的逻辑。MediaUriElement的基本逻辑是:使用MediaUriPlayer对视频进行解码,再将得到的视频帧渲染到D3DImage上。

D3DRenderer内有一个方法,用于将图像渲染到D3DImage上

复制代码
/// <summary>
/// 刷新图像
/// </summary>
protected void InternalInvalidateImage()
{
    if (!_d3dImageSource.CheckAccess())
    {
        _d3dImageSource.Dispatcher.Invoke(() =>
        {
            InvalidateImage();
        });
        return;
    }

    SetImageSourceBackBuffer(_unSetbackBuffer);
    if (!IsRenderEnable || _unSetbackBuffer == IntPtr.Zero)
    {
        //不需要刷新图像
        return;
    }
    _d3dImageSource.Lock();
    _d3dImageSource.AddDirtyRect(new Int32Rect(0, 0, _d3dImageSource.PixelWidth, _d3dImageSource.PixelHeight));
    _d3dImageSource.Unlock();
}

从原理上看,每一个视频帧都会通过这个方法来渲染,因此这个方法应该会被非常频繁的调用------在Windows上确实如此,而在wine上却只有在初始化时调用了一次。那么问题大概率就在这里,只要循着Windows上该方法的调用堆栈往上找,应该就能够定位到问题代码。

于是,定位到一段代码:

复制代码
public int PresentImage(IntPtr dwUserId, ref VMR9PresentationInfo lpPresInfo)
{
    try
    {
        lock (DeviceLock)
        {
            TestRestoreLostDevice();
            if (_d3dSurface9 != null)
            {
                var rcSrc = IsValidDsRect(lpPresInfo.rcSrc) ? lpPresInfo.rcSrc : null;
                var rcDst = IsValidDsRect(lpPresInfo.rcDst) ? lpPresInfo.rcDst : null;
                var hr = _d3dDevice9.StretchRect(lpPresInfo.lpSurf, rcSrc, _d3dSurface9, rcDst, 0);
                if (hr < 0)
                {
                    return hr;
                }
            }
            
        }

        NewAllocatorFrame?.Invoke(this, EventArgs.Empty);
        return 0;
    }
    catch (Exception)
    {
        return UnspecifiedErrorCode;
    }
}

这个方法是给DirectShow的RenderFilter(渲染筛选器)调用的,应用不调用。渲染筛选器在获取到一帧图像,需要渲染时调用该方法,通知应用进行渲染。D3DRenderer的InternalInvalidateImage就是通过NewAllocatorFrame事件的触发执行的。很显然,问题就在这几行代码:

复制代码
if (hr < 0)
{
    return hr;
}

如果StretchRect调用失败了,PresentImage方法就直接返回调用失败的代码,而不再触发NewAllocatorFrame。这说明,在wine上面,视频无法播放的原因是StretchRect调用失败了。这里没有抛出异常,而是返回了错误代码,又由于这个方法是由RenderFilter进行调用的,如果RenderFilter内得到错误代码后,没有抛出异常来,那么应用就无法得知这里出现了问题。

至于StretchRect调用失败的问题,倒是不能怪wine,因为StretchRect的文档约定,如果rcSrc或者rcDst传入null,则使用源surface或者目标surface的整个矩形。而通过阅读StretchRect的代码得知,如果传入的rcSrc或rcDst不为null而是无效矩形,则会执行失败并返回错误码(这一点在Windows上进行了验证,wine的行为和Windows是一致的)。所以这里,应用应当对无效矩形进行处理,例如,如果矩形无效,就传入null。

相关推荐
竹等寒2 小时前
Powershell 进阶语(三)
windows·安全
ITHAOGE1510 小时前
下载| Windows 11 ARM版9月官方ISO系统映像 (适合部分笔记本、苹果M系列芯片电脑、树莓派和部分安卓手机平板)
arm开发·windows·科技·microsoft·微软·电脑
百事牛科技10 小时前
PPT如何退出“只读模式”?4 类场景的实用解锁方法
windows·powerpoint
love530love11 小时前
Windows 系统部署 阿里团队开源的先进大规模视频生成模型 Wan2.2 教程——基于 EPGF 架构
运维·人工智能·windows·python·架构·开源·大模型
苦逼IT运维12 小时前
Windows 作为 Ansible 节点的完整部署流程(含 Docker 部署 Ansible)
windows·docker·ansible
AganTee13 小时前
Win11共享打印0x0000bc4,三步解决共享难题
windows·打印机·win11共享打印0x0000bc4
CH_Qing16 小时前
Windows 显示器EDID笔记
windows·笔记·计算机外设
小刘小刘可爱一流zz16 小时前
windows多显示器,独立的虚拟桌面
windows·显示器
一点都不方女士17 小时前
.NET Framework 3.5官网下载与5种常见故障解决方法
c++·windows·framework·.net·动态链接库·运行库
YCOSA20251 天前
ISO 雨晨 26200.6588 Windows 11 企业版 LTSC 25H2 自用 edge 140.0.3485.81
前端·windows·edge