找回密码
 立即注册
首页 业界区 业界 我在厂里搞wine的日子

我在厂里搞wine的日子

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

问题描述

希沃白板的汉字识别卡,正常状态下,在书写时左侧会实时更新待选汉字,类似于手写输入法;异常状态下,左侧的待选汉字列表总是为空。根据希沃白板的汉字识别卡的原理写了一个简单的Demo用于调试:
  1. using Microsoft.Ink
  2. private void ButtonClick(object sender, RoutedEventArgs e)
  3. {
  4.     Console.WriteLine("button click");
  5.     try
  6.     {
  7.         using (MemoryStream ms = new MemoryStream())
  8.         {
  9.             Console.WriteLine("new ms");
  10.             theInkCanvas.Strokes.Save(ms);
  11.             Console.WriteLine("Save strokes into ms");
  12.             var myInkCollector = new InkCollector();
  13.             var ink = new Ink();
  14.             ink.Load(ms.ToArray());
  15.             Console.WriteLine("Load strokes from ms to ink");
  16.             using (RecognizerContext context = new RecognizerContext())
  17.             {
  18.                 //...
  19.             }
  20.         }
  21.     }
  22.     catch (Exception ex)
  23.     {
  24.         Console.WriteLine($"{ex.HResult:X} : {ex.Message}");
  25.     }
  26. }
复制代码
把Demo放到Wine上运行,得到相关的wine的日志输出:
  1. button click
  2. new ms
  3. Save strokes into ms
  4. //先尝试创建进程内COM对象
  5. 0024:err:ole:com_get_class_object class {43fb1553-ad74-4ee8-88e4-3e6daac915db} not registered
  6. //再尝试从本地机器创建进程外COM对象
  7. 0024:err:ole:create_server class {43fb1553-ad74-4ee8-88e4-3e6daac915db} not registered
  8. //最后尝试从远程机器创建进程外COM对象(没实现,所以是fixme)
  9. 0024:fixme:ole:com_get_class_object CLSCTX_REMOTE_SERVER not supported
  10. //三种方式都不行,COM对象无法创建
  11. 0024:err:ole:com_get_class_object no class object {43fb1553-ad74-4ee8-88e4-3e6daac915db} could be created for context 0x15
  12. 80040154 :
复制代码
最初的思路

从日志来看,问题是缺少dll或者dll未注册导致的,0x80040154的错误码表示的是COM类型未注册(一些内部的错误代码,不知道其代表什么意思的话,可以在相关模块的wine的源码里进行搜索,比如这个0x80040154搜索到的是REGDB_E_CLASSNOTREG,然后查找REGDB_E_CLASSNOTREG的引用,结合日志,就能够知道返回错误代码的大致的原因),从错误代码返回的路径来看,是ole模块加载COM组件时,无法从注册表查找到{43fb1553-ad74-4ee8-88e4-3e6daac915db},因此最初的解决思路是把缺少的注册表从Windows上拷贝过去。
  1. [HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{43FB1553-AD74-4ee8-88E4-3E6DAAC915DB}]
  2. @="InkCollector Class"
  3. [HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{43FB1553-AD74-4ee8-88E4-3E6DAAC915DB}\InprocServer32]
  4. @="%CommonProgramFiles%\Microsoft Shared\Ink\InkObj.dll"
  5. "ThreadingModel"="Both"
  6. [HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{43FB1553-AD74-4ee8-88E4-3E6DAAC915DB}\ProgID]
  7. @="msinkaut.InkCollector.1"
  8. [HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{43FB1553-AD74-4ee8-88E4-3E6DAAC915DB}\Programmable]
  9. [HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{43FB1553-AD74-4ee8-88E4-3E6DAAC915DB}\VersionIndependentProgID]
  10. @="msinkaut.InkCollector"
复制代码
把这些注册表拷过去后,wine的日志输出如下:
  1. button click
  2. new ms
  3. Save strokes into ms
  4. 0024:err:ole:com_get_class_object class {937c1a34-151d-4610-9ca6-a8cc9bdb5d83} not registered
  5. 0024:err:ole:create_server class {937c1a34-151d-4610-9ca6-a8cc9bdb5d83} not registered
  6. 0024:fixme:ole:com_get_class_object CLSCTX_REMOTE_SERVER not supported
  7. 0024:err:ole:com_get_class_object no class object {937c1a34-151d-4610-9ca6-a8cc9bdb5d83} could be created for context 0x15
  8. 80040154 :
复制代码
日志和前面相似,只是未注册的class不一样了,class对应的注册结构和前面也一样的:
  1. [HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{937C1A34-151D-4610-9CA6-A8CC9BDB5D83}]
  2. @="InkObject Class"
  3. [HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{937C1A34-151D-4610-9CA6-A8CC9BDB5D83}\InprocServer32]
  4. @="%CommonProgramFiles%\Microsoft Shared\Ink\InkObj.dll"
  5. "ThreadingModel"="Both"
  6. [HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{937C1A34-151D-4610-9CA6-A8CC9BDB5D83}\ProgID]
  7. @="msinkaut.InkObject.1"
  8. [HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{937C1A34-151D-4610-9CA6-A8CC9BDB5D83}\Programmable]
  9. [HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID\{937C1A34-151D-4610-9CA6-A8CC9BDB5D83}\VersionIndependentProgID]
  10. @="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#封装里,有许多类似这样的接口申明和类申明:
  1. [ComImport]
  2. [TypeLibType(4096)]
  3. [InterfaceType(2)]
  4. [Guid("11A583F2-712D-4FEA-ABCF-AB4AF38EA06B")]
  5. internal interface _IInkCollectorEvents
  6. {
  7.      //...
  8. }
  9. [ComImport]
  10. [ClassInterface(0)]
  11. [ComSourceInterfaces("Microsoft.Ink._IInkCollectorEvents")]
  12. [SuppressUnmanagedCodeSecurity]
  13. [Guid("43FB1553-AD74-4EE8-88E4-3E6DAAC915DB")]
  14. [TypeLibType(2)]
  15. internal class InkCollectorClass : IInkCollector, InkCollectorPrivate, _IInkCollectorEvents_Event
  16. {
  17.      //...
  18. }
复制代码
要补充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的安装过程可以总结一个流程:

  • 解压应用安装包,找到安装包内附带的依赖包
  • 手动安装依赖包,或通过winetricks安装
  • 重新运行应用安装包
部分无法安装的应用可能能够通过这个流程进行安装,但是这个流程能够解决的安装问题非常有限,下列这类问题都是无法解决的:

  • 本身安装流程存在其他问题
  • 依赖包无法安装,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进行调试,这对定位问题有两个可能的好处:

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

简化的MediaUriElement的类图如上图所示,MediaUriElement继承自D3DRenderer,D3DRenderer内是使用D3DImage进行视频帧渲染的逻辑。MediaUriElement的基本逻辑是:使用MediaUriPlayer对视频进行解码,再将得到的视频帧渲染到D3DImage上。
D3DRenderer内有一个方法,用于将图像渲染到D3DImage上
  1. /// <summary>
  2. /// 刷新图像
  3. /// </summary>
  4. protected void InternalInvalidateImage()
  5. {
  6.     if (!_d3dImageSource.CheckAccess())
  7.     {
  8.         _d3dImageSource.Dispatcher.Invoke(() =>
  9.         {
  10.             InvalidateImage();
  11.         });
  12.         return;
  13.     }
  14.     SetImageSourceBackBuffer(_unSetbackBuffer);
  15.     if (!IsRenderEnable || _unSetbackBuffer == IntPtr.Zero)
  16.     {
  17.         //不需要刷新图像
  18.         return;
  19.     }
  20.     _d3dImageSource.Lock();
  21.     _d3dImageSource.AddDirtyRect(new Int32Rect(0, 0, _d3dImageSource.PixelWidth, _d3dImageSource.PixelHeight));
  22.     _d3dImageSource.Unlock();
  23. }
复制代码
从原理上看,每一个视频帧都会通过这个方法来渲染,因此这个方法应该会被非常频繁的调用——在Windows上确实如此,而在wine上却只有在初始化时调用了一次。那么问题大概率就在这里,只要循着Windows上该方法的调用堆栈往上找,应该就能够定位到问题代码。
于是,定位到一段代码:
  1. public int PresentImage(IntPtr dwUserId, ref VMR9PresentationInfo lpPresInfo)
  2. {
  3.     try
  4.     {
  5.         lock (DeviceLock)
  6.         {
  7.             TestRestoreLostDevice();
  8.             if (_d3dSurface9 != null)
  9.             {
  10.                 var rcSrc = IsValidDsRect(lpPresInfo.rcSrc) ? lpPresInfo.rcSrc : null;
  11.                 var rcDst = IsValidDsRect(lpPresInfo.rcDst) ? lpPresInfo.rcDst : null;
  12.                 var hr = _d3dDevice9.StretchRect(lpPresInfo.lpSurf, rcSrc, _d3dSurface9, rcDst, 0);
  13.                 if (hr < 0)
  14.                 {
  15.                     return hr;
  16.                 }
  17.             }
  18.             
  19.         }
  20.         NewAllocatorFrame?.Invoke(this, EventArgs.Empty);
  21.         return 0;
  22.     }
  23.     catch (Exception)
  24.     {
  25.         return UnspecifiedErrorCode;
  26.     }
  27. }
复制代码
这个方法是给DirectShow的RenderFilter(渲染筛选器)调用的,应用不调用。渲染筛选器在获取到一帧图像,需要渲染时调用该方法,通知应用进行渲染。D3DRenderer的InternalInvalidateImage就是通过NewAllocatorFrame事件的触发执行的。很显然,问题就在这几行代码:
  1. if (hr < 0)
  2. {
  3.     return hr;
  4. }
复制代码
如果StretchRect调用失败了,PresentImage方法就直接返回调用失败的代码,而不再触发NewAllocatorFrame。这说明,在wine上面,视频无法播放的原因是StretchRect调用失败了。这里没有抛出异常,而是返回了错误代码,又由于这个方法是由RenderFilter进行调用的,如果RenderFilter内得到错误代码后,没有抛出异常来,那么应用就无法得知这里出现了问题。
至于StretchRect调用失败的问题,倒是不能怪wine,因为StretchRect的文档约定,如果rcSrc或者rcDst传入null,则使用源surface或者目标surface的整个矩形。而通过阅读StretchRect的代码得知,如果传入的rcSrc或rcDst不为null而是无效矩形,则会执行失败并返回错误码(这一点在Windows上进行了验证,wine的行为和Windows是一致的)。所以这里,应用应当对无效矩形进行处理,例如,如果矩形无效,就传入null。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册