分类目录归档:佳佳教程

[SQLite]导入自己写的扩展出现”SqliteError: 找不到指定的程序”

自己写了个sqlite c++扩展,然后编译之后载入出现了标题上的错误,如果是英文的话应该是”SqliteError: The specified module could not be found”。

出现这个错误并不是sqlite没找到对应的dll文件,如果是没找到文件的话会提示“找不到指定的模块”。

这个情况是sqlite载入dll之后找不到程序入口,但我在 load_extension 和 c++ 代码中的入口名称明明就是一致的,于是我用 `dumpbin /exports dll文件` 查看dll到底导出了什么入口,发现导出了这么个东西:

_Z22我的入口名P7sqlite3PPcPK20sqlite3_api_routines

我把这一串名字写到 load_extension 中之后终于可以正确导入了,但我肯定不能让它一直生成这样不确定的名字,所以开始找找办法让它只导出我指定的名字。

然后发现这是我用了c++的原因,要解决dll导出的入口名和代码里写的不一致的问题,只要在 `__declspec(dllexport)` 前面加上 `extern “C” ` 即可,完整的是

extern "C" __declspec(dllexport)

这样改完再编译,使用dumpbin查看导出的函数名就已经正常了。

让docker构建nodejs应用时使用npm缓存加速安装

我的node应用在原本的部署脚本下,每次部署都要十几分钟,而我的项目又不适合多阶段构建,于是想了各种办法让它使用镜像layer缓存一部分安装过程,但是多多少少都有点问题。

看来想要不出问题的话还是要在部署的时候让安装过程完整跑一次,这样要缩短部署时间就只能尽量利用npm缓存。

第一步:挂个外部缓存

我本来以为dockerfile构建过程中是没法像容器那样绑定一个volume来让过程文件持久化的,但是昨天我才发现其实是有的,就是要更新一下docker,这是Buildkit特性的一部分,这个特性在18.09版本之后的docker才有,总之尽量把docker升级到最新版就可以用了。

使用方法很简单,如下Dockerfile:

FROM node:18.18.1-buster-slim
RUN apt update &&\
	apt install -y git openssh-client python3 curl
COPY ./deploy /deploy
COPY ./app/ /app
RUN --mount=type=cache,target=/root/.npm \   #在RUN命令的开头这样写来挂一个缓存目录,这个mount参数需要在每个要使用此缓存的命令里都写一遍
	sh /deploy/install.sh   #然后执行你的安装脚本
WORKDIR "/deploy"
ENTRYPOINT ["/bin/sh"]
CMD [ "./start.sh" ]
EXPOSE 80

这样在这个dockerfile构建过程中就会把构建容器的`/root/.npm`映射到通用的docker缓存里,而且我测试下来如果在其它镜像里也挂载同样的缓存,那么其它镜像构建的时候也可以使用该缓存,但我其它镜像的FROM镜像都是相同的,不知道如果来源镜像变了是否会影响缓存挂载。

注意:~/.npm 是linux下npm默认的缓存目录,但默认缓存目录是可以更改的,如果你的项目里修改了npm的默认缓存目录地址,那么这里也要一起改。或者如果你的平台比较特殊,npm的默认目录本来就不在这里,那么也要进行对应的修改。

除了挂载缓存(type=cache)以外,还可以绑定外部文件或目录(type=bind),相关资料在这里https://docs.docker.com/build/guide/mounts/,不过这个文档有点迷惑,没写source和target哪个是里面哪个是外面的,我在source里面写外部路径它给我报文件不存在,然后我就直接用cache了,没再继续尝试。

如果你觉得构建过程缓存的文件有问题,或者单纯想清除这些缓存,可以根据这里的方法,执行以下命令:

docker builder prune --filter type=exec.cachemount

如果没有效果,可以尝试去掉–filter及其参数。

第二步:让npm优先使用缓存(可选)

npm在安装依赖时,即使本地有缓存,也会向服务器发起请求检查每个本地的缓存有没有过期,这个过程也很漫长。其实一般即使缓存过期了也问题不大,因为正常来说同一个版本号的包其内容是不会变的,所以可以让npm优先使用本地缓存,跳过检查其在线状态,这样可以大幅减少安装时间。

方法很简单,只要给安装命令加个`–prefer-offline`参数:

npm i --prefer-offline
#也可以再加个 --verbose 参数确认是否真的使用了缓存
npm i --prefer-offline --verbose

这样折腾完了之后,应用的后续部署时间在依赖没有改变的情况下从原来的十几分钟缩短到了一分多钟,堪称火箭级加速,总算解决了一个困扰我一年多的问题。

[nodejs]同时使用devDependencies和dependencies以处理本地依赖链接

我做了一个自用的工具包,但我想把其中一部分独立出来放到其它项目里用,所以我把它们放在一个命名空间里发布了出来。

于是就出现了一个问题,本来这些包是属于一整个项目,其中的模块存在一些相互依赖,既然要分开发布那么package.json里依赖模块的目标就不能再写原来的本地路径了,需要改成发布的版本号或者项目地址。

本地开发时依赖路径填写本地路径,执行npm i会帮你直接创建一个目录链接到目标目录上,这样你在目标依赖里的改动就不需要发布或者复制就可以直接在当前项目里测试,但如果需要把依赖目标改成模块的发布版本号的话,即使手动把node_module中安装的模块删除手动创建目录链接,执行npm命令在有些情况下也会把已经创建的链接删除并重新从npm下载发布版本,这样会导致开发时不知不觉使用错误的依赖。

刚刚我测试了一下,发现一个模块可以同时出现在package.jsondevDependenciesdependencies里,而且默认情况下npm会优先找devDependencies里模块的目标是否有效,有的话就会选用这个安装,因此要解决我上面遇到的问题,其实只要把需要本地开发的发布模块同时写在这两个字段里,在devDependencies里的目标写本地路径,在dependencies里的目标写发布版本即可,这样在本地存在目标路径时npm就会优先帮你创建路径,而不是从npm下载发布版。参考如下:

{
	"devDependencies": {
		"@jialibs/utils": "file:../Utils"
	},
	"dependencies": {
		"@jialibs/utils": "^1"
	}
}

就是有两点要注意,如果被依赖的模块版本变了,要注意同步修改这个里面依赖的版本,否则其它地方从npm下载就会安装老版本的依赖,或者另一个解决方案就是像我这样直接一个大版本号或者写到次版本,这样只要大版本或次版本没变,那么别处就始终会下载最新版(已经安装过的需要清除package-lock.json,否则npm还是会按照lock里的版本安装)。

另一点要注意的是如果在生产环境为npm i命令显式添加了--omit=dev这样的参数的话,会导致在devDependencies里出现过的包都不会被安装,这种情况可以尝试把本地路径的包放到`peerDependencies`中。

[TrueNAS]解决使用USB硬盘柜导致多块硬盘被识别为一块

这篇文章不是用来帮你在TrueNAS GUI里找到不显示的硬盘的,而是使用命令创建Pool。

由于大多数USB硬盘柜或者硬盘盒会硬编码一个序列号,而且接入的所有硬盘都会提供这同一个序列号给系统(不知道这是什么脑梗逻辑),又因为TrueNAS平台本身就是靠硬盘序列号来区分硬盘的,所以就会导致它的硬盘列表里只显示你插入的其中一块硬盘。

虽然TrueNAS不显示同序号的其余硬盘,但它们在系统里却是存在盘号的,所以你其实可以直接用命令创建池。

以下操作需要执行shell命令,请打开ssh服务后通过ssh连接操作,不要使用gui的终端,因为gui的会话时间太短了。

首先要找到列表里看不到的硬盘,执行以下命令,你就会看到一堆linux给硬盘分配的盘号,比如sd[abcdefg…]这样按字母顺序排下去的

lsblk

如果你插了很多硬盘,搞不清到底是少了哪块,可以去GUI的硬盘列表里扫一遍盘号,比如我的列表里就是少了sdc,再在lsblk命令的结果里确认一下sdc设备的空间大小,确认和你加的盘相符那就是那块了。

知道盘符之后就好办了,直接找个zfs的使用说明参考一下,手动创建池即可,比如这个:https://docs.oracle.com/cd/E19253-01/819-7065/gaynr/index.html

参考:我这里做了个raid1,执行以下命令创建池,创建一个raid1池,其中使用sdc和sdd盘做镜像

zpool create 池名 mirror sdc sdd

现在你的池创建好了,但你会发现在GUI的存储标签页里看不到它,在数据集里面可以看到它,但创建数据集之后也无法共享。这是因为直接使用命令创建的池缺少一些步骤,导致这个池不仅挂载点不对,而且系统池记录中没有这个池,所以大部分操作都是不允许的。

要解决这个问题很简单,我们只要先把这个池导出(export):

zpool export 池名

然后再用GUI的导入池功能把它导入进来,池的所有功能就正常了。

最后注意,由于TrueNAS是用序列号来区分硬盘的,所以使用此方法创建的pool不要在gui中执行硬盘替换等操作,可能会出问题!相关操作请使用zfs、zpool命令操作。

 

其它歪门邪道:

如果你和我一样是让TrueNAS跑在Windows宿主的虚拟机里的,那么可以让新加入的硬盘先在宿主里格式化成NTFS后生成vhd放在里面,接着让虚拟机把vhd挂载成虚拟机的硬盘,这样就有不一样的序列号了,但这么做就多了一层处理开销,也增加了意外情况下出错的风险,个人建议尽量不要这么做,除非使用vhd对你会有其它什么好处。这么一说其实其它平台的宿主也可以用其它虚拟磁盘来搞出不一样的序列号。

阻止GooglePlay框架自动升级后导致“保护机制认证”错误再次出现

最近从我爸那意外获得了一部华为Mate60Pro,虽然我本来是不会去用这种不能root 也不能刷机的手机的,但都到手里了也不能就当摆设,正好我的小米Mix2s刷了lineageos之后长久以来蓝牙微信通话声音一直不从耳机出的问题让我困扰很久了,这次就顺便把Mate60Pro当作主用机,把Mix2s换成备用机,原本当备用机的Pixel XL就正式退役了,因为它的闪存明显开始出问题了,会出现部分照片或视频损坏的现象。

换机之后自然第一件事是把谷歌框架弄上,不然连chrome都没法用。虽然不能root,但好在网上有利用备份恢复机制把谷歌框架恢复进手机的方法。这里简单提一下就是利用华谷套件APP把该装的都装了之后,最后解决无法过play保护验证的问题,华谷里面最后一步点进去是要收费的,其实网上一查都有教程,比如这个,解决了这个问题之后会发现过一天,这个错误提示就又出现了,我反复折腾这几个app,折磨了我好几天。

主要问题出在我们自己注册GSF ID的方法可以在20版本(可能21也行)的“Google Play服务”通过验证,但该服务APP会被自动升级到最新版(目前是23),且GSF ID同时会变,不知是因为ID变了还是验证方法变了,反正在23版本下你再去按流程注册新的ID清app数据也没用,一定要退回旧版play服务去注册才能过验证。

我查了一下也有人发现这个问题,他用各种禁止相关app访问网络的设置试图防止play服务自动更新,让play服务一直保持在旧版。但这样也有问题,就是有时候就是需要新版的play服务才能完成一些操作,而且保不准什么意外就让play服务又更新了,这样就很麻烦。我也尝试了一下,不管是禁止play服务还是play框架访问网络,它都会自动更新,我总不能把play商店断网吧。

然后我发现了这个方法,我目前几天测下来还没有出问题。简单说就是在解决了“保护机制认证”的错误之后,直接用以下adb命令把“谷歌服务框架”禁用

adb shell pm disable-user 'com.google.android.gsf'

然后play服务就无法获取GSF ID了,同样我们可以在“Device IDs” app中确认GSF ID已经无法获取了。我猜原理是这样:play服务获取不到ID,就无法进入下一步验证流程,那么原本验证通过的状态就会一直被保留下来,同时play服务的版本也可以保持更新。

继续阅读阻止GooglePlay框架自动升级后导致“保护机制认证”错误再次出现

[nginx]the “listen … http2” directive is deprecated, use the “http2” directive instead

新版本nginx出现了标题上的提示,其实就是把http2配置从listen指令里分出来了,修改方法很简单,只要把原来的http2去掉,然后在下面加一个`http2 on;`就行了。

原来:

server {
    listen [::]:443 http2 ssl;
    .....
}

改为:

server {
    listen [::]:443 ssl;
    http2 on;
    .....
}

就这么简单,但我为什么要写这篇?因为我搜索这个警告的时候发现搜索结果全是清一色抄的同一篇错误的文章,那篇垃圾文章莫名其妙让你删除http2就完事了,对如何添加这个新指令只字不提,看得我来气。

解决硬盘设备变成SCSI设备后在CrystalDiskInfo里不显示的问题

前几天买了一块宏碁掠夺者GM7 4T版本,回来装到主板上的第二m.2硬盘位之后发现进系统在CrystalDiskInfo(以下简称CDI)里不显示,不管怎么调设置怎么重新扫描都不出来,于是我看了一下磁盘精灵(DiskGenius),发现里面竟然显示这个盘的接口是SATA,我知道这肯定是这软件的判断错误,但不知道到底是哪里出了问题。

网上查了下,说和AHCI还有RAID模式有关,进bios里看了一下它里面关于AHCI什么的设置只和SATA接口的硬盘有关,而且调了也没解决这个nvme ssd被识别成SCSI设备的问题,于是我怀疑是不是这个第二m.2接口接上去就是会变成这样的,我就把主m.2槽的系统盘和它对调了一下,这下好了,俩盘都变成SCSI设备了。然后我还作死尝试删除驱动让它重新识别成nvme设备,结果就是一个下午加上晚上3小时我都在修复系统,因为转完loading的圈圈电脑就会直接重启,看了一下启动日志是加载完disk.sys之后其它的所有驱动就都无法加载了,显然是加载了disk驱动之后对硬盘的读写就出现了错误,当然和我删除的驱动有关。

后来我好不容易通过其它电脑开虚拟机把这个盘里的系统覆盖安装了,装回主机里成功启动后打开CDI看了一下发现所有盘都扫描不到了,我想起来是bios里把ACHI切成RAID了,但现在切回ACHI我的系统又进不去了,我开始怀疑会不会其实有一个驱动可以让硬盘就在SCSI控制器下正常工作,所以我翻了一下AMD官网上我这个主板芯片组的驱动列表,发现确实有和raid有关的驱动,装好之后所有的硬盘就都可以在CDI里显示出来了。我觉得这里要批评微星的主板驱动列表,放驱动也不放全,如果他那有这驱动我一开始就打上了,就没后面那么多事情了。

虽然现在我所有的硬盘都不小心被我变成SCSI设备了,不过只要装好驱动其实也是照常用的,也能正确获取SMART信息。最开始我发现网上也有不少人问硬盘变成SCSI设备了怎么改回来,因为没相关驱动的时候CDI等软件是无法获取硬盘的SMART信息的,这样对于硬盘的健康状态就心里没底,虽然我还是不知道要怎么改回来,不过把RAID驱动装好也是一个解决方案,虽然我们用不上它raid的功能,但它提供了获取这些信息的接口。

[node.js]同时使用canvas模块和sharp模块出现“The specified procedure could not be found”错误

升级到最新的node之后这俩模块是彻底没法共存了,之前还能通过降级其中一个模块来兼容一下,现在我怎么试都没法成功运行了,于是尝试找解决方案。

然后我发现这是由于两个模块使用的同名链接库不兼容导致的其中一个模块报错,似乎就是个无解的问题,偶然发现一个替代方案:https://github.com/Brooooooklyn/canvas

如果本来只使用了标准的绘制功能,那么可以连代码都不用改,直接替换掉依赖就可以解决问题,如果使用了非标准canvas方法,那么可能需要做一些小修改,比如将图片导出为png等格式时的额外参数之类的情况。

基本上要进行的操作就是删除`package.json`中的`canvas`依赖那行,然后直接执行`npm i @napi-rs/canvas`,npm就会自动删除`canvas`模块并安装`@napi-rs/canvas`,然后把代码中的`require(“canvas”)`换成`require(“@napi-rs/canvas”)`即可。

使用css grid制作一个活跃度砖墙

我也不确定这个到底叫什么,就是Github那种根据提交的代码次数改变方块颜色的动态墙,在这里暂时就叫活跃度砖墙。

这篇文章早就想发了,不过最近沉迷看番,一直都没写,我制作的版本在这里可以看到:https://blist.9baka.cc/user/luojia

基本思路就是往一个 `display` 为 `grid` 的容器里塞方块,让 `grid `的 `grid-auto-flow` 为 `column` ,这样方块就是纵向排列的了,然后用 `grid-template-rows: repeat(8, 1fr)` 把行数分成固定的8份,列数会自动根据格子数量增加,最后对所有砖头使用 `aspect-ratio: 1/1` 来把它们固定为正方形(当然你要别的形状也可以),样实现的墙里面的格子可以自动根据整体的宽度改变大小。

和Github以及大多数地方不同的是,我用颜色把每个月区分开了,这样看起来更直观一些。

效果如图

继续阅读使用css grid制作一个活跃度砖墙

[MySQL]在一个update语句中对不同的行更新不同的数据

mysql中update的基础用法是update 表 set 列1=?,列2=?,... where 筛选条件,这样的语句一次只可以对符合条件的所有行更新相同的数据,但update语句还可以join其它的表,从别的表读取数据更新到对应的行上,这样就可以实现对不同的行更新不同的数据,如下:

update
	表名 as t1
	inner join 表2 as t2      #也可以left join 或 right join 或 join,这里的join和select里面那个join用法是一样的
	on t1.id列名=t2.id列名
set
	t1.列1 = t2.列1,
	t1.列2 = t2.列2

虽然上面的方法能实现标题的要求,但总不能先把数据插入一个表再这样update吧,于是我想了一会儿,想到如果要提交多组数据肯定要把数据结构化,如果要提交结构化数据那肯定是要用json,mysql 的json函数里有一个`JSON_TABLE`函数可以用来把一个json转换成一个表类型,那么这个方法就好实现了,只要把上面的表2替换成用JSON_TABLE生成的表,就可以通过直接向mysql提供一个json来对不同的行更新不同的数据了,如下:

update
	表名 as t1
	inner join JSON_TABLE(
		?,
		'$[*]' COLUMNS (    #把json作为一个数组遍历,提取每个子数组作为数据源
			id列名 BIGINT PATH '$[0]',    #提取子数组第一个元素,取名为"id列名",类型是BIGINT,建议取名为目标表中的id列名,这样方便用using
			列1 INT PATH '$[1]',          #提取子数组第二个元素,取名为"列1",类型是INT
			列2 VARCHAR PATH '$[2]'       #提取子数组第三个元素,取名为"列2",类型是VARCHAR
		)
	) as t2 using(id列名)       #这个using是前一个例子中on语法的一种简写,用法参考官方文档
set
	t1.列1 = t2.列1,
	t1.列2 = t2.列2;

在 ? 的位置填入一个json字符串或者prepare这个语句后提交一个json字符串,就可以啦。json字符串示例如下:

[
	[1,20,"张三"],
	[2,24,"李四"],
	[3,18,"王五"],
	[4,23,"赵六"],
	[5,55,"小二"],
	[6,60,"poi"]
]

这样每个子数组中的第一个项目被提取为”id列名”,第二个为”列1″,第三个为”列2″(都是在上面的sql里的JSON_TABLE里取的名字),对应的值在set语句里被赋值到对应的行上,功能就完成啦。

docker中的wordpress提示“您的站点不能完成环回请求”

我一直到刚刚才发现我的blog已经好久没给我发送自动备份了,然后后台里找了找原因,发现在“站点健康”页面显示“您的站点不能完成环回请求”,然后下面还有别的提示表示因为这个原因,定时任务无法启动。

于是我开始找为什么它会无法发送请求给自己,进到容器里用curl -v请求本站,发现域名解析到了容器的IP地址,但容器上开放的端口并不是443,所以会出现“Connection refused”错误。

然后再继续找为什么域名会被解析到容器ip,猜想肯定和docker的某些策略有关,搜了一下,原来是因为我给它使用了自定义网络,和默认网络不同的是自定义网络里容器名会被用于同网络中域名的dns解析,我的容器名就是luojia.me,结果就是wordpress请求自己的地址时被解析到了容器ip,导致请求失败。

解决方式就是把容器名换掉,比如加个前缀之类的就解决了。

[vue-router]点击链接导航后组件不刷新

这一个奇怪的bug困扰了我2天半,浪费了我很多时间来反复修改调试找问题,最后虽然找到了问题点,但是依然不知道是怎么发生的。

和网上很多其它能找到的页面不刷新的问题不同,他们要么是忘了写`<router-view>`,要么就是靠给`<router-view>`加key来解决缓存问题。而我这里的症状是页面加载后直接显示了最后一个路由的内容,不管点哪个链接都会显示最后添加的路由组件的内容。

最后我在控制台打出了所有组件的渲染函数和data函数才发现,并不是vue在渲染时选错了渲染对象,而是所有组件的渲染函数和data函数都直接指向了最后一个路由的函数,但我并没有代码去动态修改这些组件的函数,于是我在某个组件的data函数里填了一点数据再测试,问题就解决了。

由于是测试组件,原本data返回的对象是空的,像这样:

export default {
	data() {
		return {

		};
	},
}

填完数据之后变成这样:

export default {
	data() {
		return {
			poi:'poipoi',
		};
	},
}

然后这一个组件里面就不会自动变成最后一个路由组件的render函数和data函数了,但如果两个组件里面data返回的对象填写的内容也一样,那么前一个组件里面的这两个函数依然会指向后一个的,这在代码逻辑上怎么都讲不通,只能说应该是在某个优化环节出问题了,把字面一样的函数直接简化成了同一个,然后就产生了这种所有路由组件全部渲染出最后一个组件的内容的情况。