分类目录归档:Platform

Qt 通过自定义 URL Scheme 给已经运行的应用传参(Windows&macOS)

在传统的应用程序设计中,我们可以看到很多通过浏览器唤起本地应用的案例,比如百度网盘、迅雷等工具,他们在浏览器访问一个非 http/https 协议开头的地址时,会自动打开其自己的应用程序并传递一定的参数。该功能的实现方式网络上有很多示例,在 Windows 和 macOS 不同平台下他们分别需要如下设置:

继续阅读

使用 CMake + Git Submodule 方式管理项目三方库

在项目开发过程中,避免不了要使用一些开源的三方库,我参加过的一些团队有不同的管理三方库的方式。不同的方式都各有优缺点,我们先列举一下碰到过的管理方式,说一些他们的优缺点,最后再来讨论我们今天介绍的管理方式弥补了哪些缺点。

统一管理三方库

一些团队是直接将三方库编译好放到 CI 机器上,在本地开发或者 CI 出项目的 Release 包时去做依赖拷贝。这样做有以下几点好处:

  1. 三方库不需要每次都重新编译,需要时拷贝,包括符号调试文件也可以一并维护。
  2. 大家公用一份三方库,确保三方库的出处都是一样的。

但是这样管理也有一些不足。

  1. 如果不同的项目需要依赖不同版本、不同分支的三方库,则统一的管理无法满足需求
  2. 三方库的版本管理迭代会保留诸多历史文件,一旦历史版本被清理则很难追溯历史缺陷的符号调试文件

上传三方库文件到 Git 中

编译好直接上传到项目的仓库中,这种方式有一些好处,比如:

  1. 工程下载下来不需要多余配置,启动 IDE 直接编译不废话。
  2. 自己想依赖什么版本就什么版本,不受限于其他项目

当然缺点显而易见:

  1. 静态库文件编译后非常大,上传到 Git 仓库中仓库会异常庞大,特别是三方库二进制文件的更新。
  2. 这种形式很容易出现源码和三方库文件不匹配的情况,这个项目编译好的复制到其他项目直接用,管理混乱。
  3. 跨平台项目要上传不同平台的三方库二进制文件到工程中

使用 CMake + Git Submodule 形式管理

通过以上的描述,我们已经很容易能看清楚不同的管理方式的优劣了。本文推荐的方法可以弥补一些上面方式的不足,但可能也因不同团队而异。推荐的方式大概分以下步骤。

  1. 将你需要依赖的三方库设置为 Git Submodule,每个项目可以依赖不同版本的三方库,不需要上传二进制文件。
  2. 脚本利用 CMake 自动编译三方库工程,不同平台可以使用 CMake 自动编译,不需要单独维护个别版本。
  3. CI 出 Release 时自动编译三方库并保留匹配版本的调试符号文件,和本地源码原始匹配不易混乱。

我们以一个 libyuv 三方库举例。首先将项目添加为 Git Submodule

git submodule add https://chromium.googlesource.com/libyuv/libyuv third_party/libyuv

添加完成后,我们写一个自动编译三方库的脚本,Windows 平台下使用批处理、macOS 下使用 Shell 脚本。

# Windows: Generate visual studio solution
cmake -H. -Boutput -G"Visual Studio 15 2017"
# Build release library
cmake --build output --config Release --target yuv
# Build debug library
cmake --build output --config Debug --target yuv
# macOS: Generate makefile
cmake -H. -DCMAKE_MACOSX_RPATH=OFF -DCMAKE_BUILD_TYPE=Release -Boutput -G"Unix Makefiles"
# Static library
cmake --build output --target yuv

这样在不同平台我们就可以自动编译三方库二进制文件了,本地开发时只需要将 Submodules 更新下来,头文件和库文件路径都修改为三方库文件的路径。CI 出 Release 包时也是先将 Submodules 更新下来编译后再编译项目主体。

NSIS 打包 Electron 项目的注意事项(拖拽、权限、兼容性等)

我自己本人不做 Electron 的开发,但有一位合作伙伴在使用 NSIS 打包 Electron 应用的时候遇到了一些问题,主要问题有以下几个,先记录下来,然后追个击破。

  • 打包后应用在 Windows 7 无法直接运行,需要修改兼容性为 Windows 7 才可以使用
  • 打包后安装或者卸载时应用在运行会安装或者卸载失败,无法替换或删除应用(这个与 Electron 无关但也介绍一下)
  • 打包后应用第一次启动无法使用拖拽功能

继续阅读

Qt for iOS 应用使用自定义动态库 Framework

本文内容适合当你想使用 Qt 调用 xcode 开发的动态库 framework 做 iOS 应用的场景,文中涉及到原始动态库 framework 文件的配置,以及 Qt 如何引入和打包动态库 framework 到自身程序中的方法。

动态库 framework 的配置

默认 xcode 创建的 framework 都已经是动态库形式了,所以这里不多介绍,请确保你的 Build Settings 中以下设置是无误的,特别是 Dynamic Library Install Name Base,它决定了动态库能否被应用成功搜索到。

对于 @rpath 的说明,请看这里:https://www.cnblogs.com/csuftzzk/p/mac_run_path.html

Qt 项目配置引入 framework

Qt 引入 framework 只需要给 LIBS 追加 framework 的路径和要链接的文件就可以了,配置如下,请注意使用 framework 参数是 -F 和 -framework(注意大小写)

LIBS += -F$$PWD/../../build/Products/Release
LIBS += -framework my_dylib.framework

这样引入还是不行的,因为 iOS 应用部署上去以后动态库的 framework 文件是不会跟随打包进去的,所以你还需要做一件事情就是把 framework 打包到你的应用中,配置如下:

MY_DYLIB_FRAMEWORK.files = $$PWD/../../build/Products/Release-iphoneos/my_dylib.framework
MY_DYLIB_FRAMEWORK.path = /Frameworks
QMAKE_BUNDLE_DATA += MY_DYLIB_FRAMEWORK

分别指定了 framework 文件的路径和要被打包进去的目标路径,这里是将 $$PWD/../../build/Products/Release-iphoneos/my_dylib.framework 这个 framework 打包到了应用的 /Framework 目录下。最终你看到的应用目录结构是这样的

├── Default-568h@2x.png
├── Frameworks
│   └── my_dylib.framework
│       ├── Info.plist
│       ├── ReadMe.txt
│       ├── _CodeSignature
│       │   └── CodeResources
│       └── my_dylib
├── Info.plist
├── LaunchScreen.nib
├── PkgInfo
├── _CodeSignature
│   └── CodeResources
└── cross-platform-demo

可以看到,mu_dylib.framework 文件已经在我们应用的 /Framework 目录下了,这样文件就被打包进去了,而且当你使用 Qt 编译程序的时候,在 Build Shadow 目录下会看到生成对应的 xcodeproj 文件,可以直接使用 xcode 打开,使用 xcode 打开项目后可以看到项目的 Build Phases 下面多了一条 Copy file to bundle 的项目:

但是当你尝试在 iOS 或者模拟机中运行这个应用时你会发现又有新的错误了,如下所示:

dyld: Library not loaded: @rpath/my_dylib.framework/my_dylib
  Referenced from: /var/containers/Bundle/Application/D0143CDE-FFFE-4343-BFD9-D70DA66C831F/cross-platform-demo.app/cross-platform-demo
  Reason: image not found
program received signal 6, thread:15362b

我明明已经把文件复制进去了,为什么还是会提示,还记得上面我们提到的 Dynamic Library Install Name Base 吗?这是动态库 framework 设置的,根据上面文章的资料,我们要在调用该模块的应用中设置 rpath 的搜索范围,让其能找到我们的动态库文件。Qt 项目中添加如下配置:

# 添加应用的 runpath 路径,因为 my_dylib 动态库 Framework 设置的 install path 为 rpath,所以应用使用时需要单独设置
QMAKE_LFLAGS += -Wl,-rpath,@loader_path/Frameworks

如此设置后,在 Qt 中就可以成功编译程序并运行在模拟器或真机上了,如果还有任何疑问欢迎留言我们一起讨论。

wordpress 异常访问 wp-login.php?action=lostpassword 导致站点流量异常无法访问

今早起来看了下博客的内容,发现站点打不开了,想想不对,昨天刚换好的服务器,怎么忽然就打不开了?ping 了一下服务器地址是通的,但是延迟非常高。随后 ssh 登录到服务器后查看站点的日志,发现有一个上海的 IP 大量的请求地址 wp-login.php?action=lostpassword,对 wordpress 的代码不是很了解,但看这个地址应该是一些暴力破解用着相关漏洞利用。

通过日志可以看到是一个 210.52.224.* 的 IP 段,查了一些资料,在 /etc/rc.local 中添加整个 210.52.224.* 的 IP 段屏蔽他们的请求,随后站点恢复正常:

#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.

iptables -I INPUT -s 210.52.224.0/24 -j DROP

如果你也遇到类似问题,在文件末尾像我一样增加这个 IP 段,然后重启服务器就可以屏蔽他的连接了。

利用 WM_NCHITTEST 消息自定义窗口可拖动区域

最近接到一个比较个性化的需求,原来团队基于 Electron 开发的项目由于不支持 XP 系统,想完全移植到基于 CEF 2623 版本的空壳中,以此来兼容 XP 系统。而程序的界面则完全交由前端来处理了,比如标题栏、最小化、最大化等功能按钮。这其中就涉及到一些原生应用的功能比如拖动窗口的功能是需要原生窗口提供的,微软提供拖动窗口的机制如此文章所描述:https://www.cnblogs.com/GnagWang/archive/2010/09/12/1824394.html。为了保证内容不丢失,我特意截图保留一份。

继续阅读

VS2017 打开并编译 cef client 项目进行调试

最近接触 cef 非常多,有些功能没有做过就去 cef 官网的 cef_client demo 中查找示例,所以第一步是先把他给编译通过,网络上看过一些方法和例子需要单独下载 CMake,其实 VS2017 自带了这个功能。在你安装 VS2017 的时候默认勾选了 CMake 工具集。我们直接使用 CMake 工具打开项目就可以了,如下所示:

微软官方的教程:https://docs.microsoft.com/zh-cn/cpp/ide/cmake-tools-for-visual-cpp?view=vs-2017

用 VS2017 打开 CMakeLists.txt 文件

然后选择 cef_client 的客户端 CMakeLists.txt 文件:

随后项目会进行初始化:

点击上方生成的类型下拉框点击管理配置,在弹出的窗口中选择 x86 Debug

选择后 VS 会在 CMakeLists.txt 目录下创建一个 CMakeSettings.json 的配置文件,这个文件保存了项目配置信息以后可以自己手动随时修改。在选择启动项的下拉菜单中选择启动 cef_client 项目:

然后按下 F7 开始编译项目,生成完成后点击上面的绿色启动按钮就可以启动项目了,并且你也可以随时调试项目了。

VC++ 串口开、关、读、写操作及注意事项

最近帮朋友做一款工具,设计到对操作系统串口的操作,虽然这个东西已经是历史产物了,但是还有很多设备再用,索性从网络上找了一些代码最终完成这个小功能。下面资料将介绍串口在打开、关闭、读和写的时候一些注意事项以及参数的配置(代码中有详细注释。)

串口的开关

在串口打开的时候,我们要对串口做一些基础的初始化,比如波特率、数据位、校验位、停止位几个参数,他们分别被声明在 WinBase.h 头文件中。

打开串口的代码如下:

bool SerialPortManager::Open(ReceiveDataCallback cb/* = nullptr*/)
{
    if (serial_handle_ != NULL)
    {
        return false;
    }

    if (cb != nullptr)
    {
        cb_ = cb;
    }

    serial_handle_ = CreateFile(com_.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
    if (serial_handle_ == INVALID_HANDLE_VALUE)
    {
        return false;
    }

    // 获取旧的 dcb 数据
    DCB dcb;
    dcb.DCBlength = sizeof(DCB);
    if (!GetCommState(serial_handle_, &dcb))
    {
        Close();
        return false;
    }

    // 修改 dcb 数据然后设置端口属性
    // CBR_115200;
    dcb.ByteSize = byte_size_;
    dcb.BaudRate = baud_rate_;
    dcb.StopBits = stop_bits_;
    dcb.Parity = parity_;
    dcb.fBinary = TRUE;
    dcb.fParity = TRUE;
    if (!SetCommState(serial_handle_, &dcb))
    {
        Close();
        return false;
    }

    // 设置读写缓冲区大小
    SetupComm(serial_handle_, 1024, 1024);

    // 清空数据
    PurgeComm(serial_handle_, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR);

    // 设置超时 10 秒
    COMMTIMEOUTS to;
    memset(&to, 0, sizeof(to));
    to.ReadIntervalTimeout = 1000;
    to.ReadTotalTimeoutMultiplier = 500;
    to.ReadTotalTimeoutConstant = 5000; //设定写超时
    to.WriteTotalTimeoutMultiplier = 500;
    to.WriteTotalTimeoutConstant = 2000;
    SetCommTimeouts(serial_handle_, &to);

    PostReadThread();

    QLOG_APP(L"Serial port device is ready, serial: {0}, baud rate: {1}, byte size: {2}, parity: {3}, stop bits: {4}.")
        << com_ << baud_rate_ << byte_size_ << parity_ << stop_bits_;

    return true;
}

其中除了打开串口时传递的参数外,还包含了一些串口处理数据超时、读写缓冲区大小等属性,需要用到的根据自己的环境来配置。 串口的关闭很简单,只需要关闭掉 CreateFile 返回的句柄就可以了,这里不多介绍。

串口读写

串口的读写可以同步也可以异步,但是同步方式会造成一个问题就是当你调用了 ReadFile 在等待串口数据时,再去调用 WriteFile 就会被阻塞,因为 ReadFile 一直没有返回。所以我还是推荐大家用异步方式来读写串口,代码如下:

void SerialPortManager::ReadSerialPortThread()
{
    QLOG_APP(L"PostReadThread is running....");
    while (TRUE)
    {
        if (!serial_handle_)
        {
            QLOG_ERR(L"Failed to read data from serial port, serial port handle is null.");
            break;
        }

        // 计算最小需要读取的数据量
        DWORD read_size = 1024;
        // read_size = min(read_size, (DWORD)com_stat.cbInQue);

        // 开始异步读取
        OVERLAPPED over_lapped;
        memset(&over_lapped, 0, sizeof(OVERLAPPED));
        over_lapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

        std::shared_ptr<BYTE> buffer;
        buffer.reset(new BYTE[read_size]);
        BOOL bReadStatus = ReadFile(serial_handle_, buffer.get(), read_size, &read_size, &over_lapped);
        if (!bReadStatus) // 如果 ReadFile 函数返回 FALSE
        {
            DWORD last_error = GetLastError();
            if (last_error == ERROR_IO_PENDING)
            {
                QLOG_APP(L"Read file return ERROR_IO_PENDING..");
                BOOL bRet = GetOverlappedResult(serial_handle_, &over_lapped, &read_size, TRUE);
                if (bRet)
                {
                    // 返回 true 代表读取到了数据
                    QLOG_APP(L"Read data {0}") << nbase::UTF8ToUTF16((char*)buffer.get());
                    cb_((char*)buffer.get());
                    PurgeComm(serial_handle_, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR);
                }
                else
                {
                    // 返回 false 可能是句柄已经被 close 了
                    QLOG_APP(L"GetOverlappedResult returned false");
                }

                continue;
            }

            QLOG_ERR(L"Failed to read data from serial prot, error code = {0}") << last_error;
        }
    }

    QLOG_APP(L"PostReadThread is quit....");
}
bool SerialPortManager::WriteData(const std::string& data)
{
    QLOG_APP(L"Begin to write data [{0}] to serial port.") << data;

    DWORD bytes_written = data.size() + 1;
    OVERLAPPED over_lapped;
    memset(&over_lapped, 0, sizeof(OVERLAPPED));
    over_lapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    BOOL write_stat = WriteFile(serial_handle_, data.c_str(), bytes_written, &bytes_written, &over_lapped);
    if (!write_stat)
    {
        DWORD last_error = GetLastError();
        if (last_error == ERROR_IO_PENDING)
        {
            WaitForSingleObject(over_lapped.hEvent, 2000);
            return true;
        }

        return false;
    }

    QLOG_APP(L"Finished to write data.");
    return true;
}

读因为是异步操作,我们需要传一个 OVERLAPPED 结构体到 ReadFile 的最后一个参数。写也是一样,我们可以用 GetOverlappedResultWaitForSingleObject 来等待操作事件完成(记得要初始化 OVERLAPPED 否则会报错的)。两种方式读写我都做了演示,可以根据自己的需求改造。 读写操作的时候可以获取当前返回值判断是不是 ERROR_IO_PENDING 来确定是不是有数据还没有读取完成。

总结

串口的读写其实还是相对简单的,上面代码基本上把可能出现问题的点都体现出来了,最后再来罗列一下注意事项。

  • 打开串口时要根据硬件情况初始化串口参数(在 WinBase.h 中有声明)
  • 设置串口的缓冲区和超时
  • 异步去读写串口通过返回值判断是否读写成功
  • 不要忘记初始化 OVERLAPPAD 结构
  • 读取完成后 PurgeComm 串口

利用 NvAPI 设置数字振动数值

开始是一位朋友有这个需求,他给了我一个英伟达官网的开发包,名字是:R410-developer.zip(诸位可以自己到英伟达官网下载),里面提供了一些示例,包含自定义分辨率、显示器颜色设置等,但是显示器色彩设置的例子一致没有跑通,而且我也没有找到哪个参数是可以设置数字振动值的,所以憋屈了很多天。但直到看到了一个 AHK 版本实现的设置工具通过代码发现,其实实现方法是通过 nvapi.dll 动态库导出的一个查询函数地址的方法,将指定接口导出来执行具体业务。再加上在 github 上搜索的各类示例,最终实现了这个功能,下面详细介绍实现步骤。

继续阅读

用于 Windows 下的日志跟踪分析工具(Tail for Windows)

在 Linux 下做开发和调试任务的时候,有些情况会动态去跟踪一些日志的变化来调试问题。Linux 下使用 tail -f 就可以达到需求了,但 Windows 下一直没有找到类似的好用工具,在 github 上也有一些开源项目,不是项目相对陈旧界面丑陋,就是功能不完善不能让人专注于分析日志。索性自己做了一个,预览图如下:

继续阅读