Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

最近发现博客上那个阅读次数的功能有点问题,因为是基于leancloud的功能,然后前几天打开了下 leancloud.app 这个网站,发现打不开了,以为是不提供服务了,因为当时刚好没精力就没仔细去研究,后面点开具体一篇文章,提示说Counter未初始化,本身这个功能是基于 leancloud 实现的计数器,需要在博客构建的时候初始化本次新增文章的一个类似于键值对的计数对。
第一步思考的错误是网站搞错了,leancloud 的官网地址是 leancloud.cn,leancloud 的正确官网是可以打开的。
那么第二步的思考是可能换了接口或者调用方式,因为打开以后要求验证手机号码,以为是也加强了审核,需要通过手机绑定后才能继续访问,
第二步这个手机验证完成后,发现还是不能够正常使用,开始注意具体的提示信息,是否真的就只是counter没初始化
因为我的博客是会同步推送到自己的另一个 gitea 服务,最近应该是入口 traefik 的状态有点异常,导致无法同步到gitea服务,但是不影响正常推送到github的page服务
第三步的问题随着这个思考就想到了,因为hexo在部署推送的时候是逐个串行地往目标部署点推送的,比如我有github跟gitea,还有个其他的部署点,那么假如前面gitea的出现了问题,后续的就没法往下推送了,正巧 leancloud 的这个计数服务是通过部署点实现的,在部署日志中会有

1
INFO  Deploying: leancloud_counter_security_sync

那么问题就差不多解决了,只是 traefik 这个问题还要看下,尝试把 traefik 的 docker-compose 启动起来,发现 80 端口被占了,
这时候就又是一个经典问题了,首先是找到哪个进程占用了这个端口

1
lsof -i:80

通过这个命令也可以,当然也可以通过 netstat,只是我一直记不住,用的不太多
发现是这个 apache2,就是重启ubuntu后默认启动的 apache 的 http 服务器,
只是这里需要注意下,它的服务名是 apache2 ,而不是httpd
所以可以通过

1
systemctl stop httpd

来关闭,同时可以用

1
systemctl disable httpd

直接把这个自启动的给干掉,省得每次重启都有这个问题,这一连串的排查还是比较有意思的

之前写了这么一篇标题党,只是这个的确是比较头疼的事情,brew更新了下,php就不能用了,这里面主要是 icu4c 这个库的更新导致的,比如最近我又碰到了
, 正好又解决了下
因为后续brew在 m系列芯片的mac上有了更新,所以之前那篇需要有一些改动,
首先是这个目录 $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
目前这个替换符 $(brew --prefix) 还是有效的,不过路径变更为了 /opt/homebrew,但是后面的路径改变了,变成了 /Library/Taps/homebrew/homebrew-core/Formula
中间少了 Homebrew , 这是第一点,
第二点是对于这个文件 icu4c.rb 的重新安装
因为目前我还在使用 php 7.4 版本,依赖的是 icu4c 的 71 版本,所以就切换到类似于 e3317b86c11c644e88c762e03eb7b310c3337587 这个 commit id 这样,

1
git checkout -b icu4c-71  e3317b86c11c644e88c762e03eb7b310c3337587

但是由于目前的brew已经没有 switch 命令了,所以只能把最新版本的卸载掉,再进行安装老版本
先通过

1
brew uninstall icu4c

卸载老版本的 icu4c
然后再使用

1
brew reinstall ./icu4c.rb

安装 71 版本的,这样就能解决这个问题
第三个问题会在安装时出现
类似于

1
attestation verification failed: Failure while executing; `/usr/bin/env GH_TOKEN=****** /usr/local/bin/gh attestation verify /Users/xxx/Library/Caches/Homebrew/downloads/

这样的操作,可以通过设置常量的形式来解决

1
export HOMEBREW_NO_VERIFY_ATTESTATIONS=true

这样就不去做这个验证了

前两天拜读了章亦春大佬的关于Dynamic Tracing的文章,觉得对现在碰到的一些问题有了一些新的思考,为了能有所产出就先写一点简单的学习记录
首先这个systemtap类似于一个linux系统层面的探针工具,可以让用户去监控系统的各种活动
以阿里云的 ubuntu 22.04 为例,
首先我们需要安装 systemtap-sdt-dev 这个包,
然后因为通过c的方式比较常规,我是想研究下针对像 php 这种怎么去监控
我们需要一个编译参数带上--enable-dtrace 的php版本
然后在编译完后我们写一个简单的测试脚本

1
2
3
4
5
6
<?php
function a($b, $c) {
return $b + $c;
}
sleep(1);
echo a(1, 2);

这其实主要是为了触发系统调用
然后重点就是systemtap的脚本,它有点类似于c的代码,结构主要是 “事件” - “处理器”
比如我监听到了php发起了一个系统调用,那这个时候我想把它记录下来,打印日志这种

1
2
3
4
5
6
probe syscall.*
{
if (execname() == "php") {
printf ("%s(%d) open, call %s \n", execname(), pid(), name)
}
}

这里就是针对所有的系统调用,如果是由php发起调用,那就打印出来pid和系统调用的名字(name就是这里的系统调用名字)

事件

syscall.system_call 只是其中一类事件
还有对虚拟文件系统的操作 vfs.file_operation
内核的方法调用 kernel.function("function")
比如 probe kernel.function("*@net/socket.c") { }
还有可以是内核的一些追踪点
kernel.trace("tracepoint")
比如 kernel.trace("kfree_skb") 这个表示每次当内核中的network buffer被释放的时候触发
还有的是时间时间
timer events
比如

1
2
3
4
probe timer.s(4)
{
printf("hello world\n")
}

每4秒打印一个 hello world
还有这些

1
2
3
4
5
timer.ms(milliseconds)
timer.us(microseconds)
timer.ns(nanoseconds)
timer.hz(hertz)
timer.jiffies(jiffies)

处理器

最常规的一类就是打印信息了
printf ( ) Statements
这个跟c语言也很像,可以用%s%d 作为占位符来填充字符串和数字
然后就是一些定义好的取值方法
execname() (a string with the executable name)
就比如刚才的php,
线程id

1
2
tid()
The ID of the current thread.

用户id

1
2
uid()
The ID of the current user.

比如系统调用就直接用 name
还有很多可以研究,第一篇学习就先到这里

这篇还是javassist的一些使用小技巧,我们可以用javassist来读取java的注解信息
首先我们有这样一个注解

1
2
3
4
public @interface Author {
String name();
int year();
}

注解可以打在类上,那么我们建一个point类

1
2
3
4
@Author(name = "nick", year = 2024)
public class Point {
int x, y;
}

name 是 nick,24年的,

然后我想通过javassist来获取这个类上的注解信息
我们先从对象池中获取这个类

1
CtClass cc = ClassPool.getDefault().get("org.example.Point");

然后获取类上的注解

1
2
Object[] all = cc.getAnnotations();
Author a = (Author)all[0];

将第一个注解强转成Author,因为这里我们知道是只有这个注解,否则可以做的更通用一点
然后把这个主机上的值获取出来

1
2
3
String name = a.name();
int year = a.year();
System.out.println("name: " + name + ", year: " + year);

可以获得结果,非常简单

1
2
3
name: nick, year: 2024

Process finished with exit code 0

为什么讲这么个简单的例子呢,因为这个刚好可以作为一种实现aop的途径,我们可以获取类的信息,包括注解上的,这样我们就可以从源头去理解这些aop啥的可以通过什么方法来实现

上次说了可以改写类,那进一步的我们可以做一下类似于之前提过的通过字节码来做切面的工作
首先我们有一个很简单的类和方法

1
2
3
4
5
6
7
8
public class DemoService {
public List<String> queryList(String name, String title, String n3) {
List<String> list = new ArrayList<>();
list.add(name);
list.add(title);
return list;
}
}

就是一个方法,输入几个字符串,然后加到list里,那么比如我们想知道打印入参和返回结果
这里比较类似于我们在用aop的时候就是织入代码
正好javassist有这样的操作接口
就是我们可以找到方法,然后使用

1
javassist.CtBehavior#insertBefore(java.lang.String)

1
javassist.CtBehavior#insertAfter(java.lang.String)

就可以在我们找到类和方法以后

1
2
3
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(targetClass);
CtMethod method = cc.getDeclaredMethod(targetMethod);

对方法的前后进行逻辑织入

1
2
3
4
5
6
7
8
9
10
11
12
method.insertBefore(
"{ java.lang.StringBuilder _paramValues = new StringBuilder();" +
" Object[] params = $args;" +
" if(params != null) {" +
" for(int i=0; i<params.length; i++) {" +
" if(i > 0) _paramValues.append(\", \");" +
" _paramValues.append(String.valueOf(params[i]));" +
" }" +
" }" +
" System.out.println(\"[Method Entry] " + targetMethod + " params: \" + _paramValues.toString());" +
"}"
);

这里我们使用了直接织入代码的形式,在javassist中可以用 $args 来获取参数,然后循环参数, 把每个参数拼接以后作为字符串输出
然后我们想要在函数返回前获取返回值,这个也是有个语法糖 $_

1
2
3
4
5
6
7
StringBuilder after = new StringBuilder();
after.append("{")
.append(" java.util.List _result = $_;\n")
.append(" String _resultStr = (_result != null ? _result.toString() : \"null\");\n")
.append(" int _size = (_result != null ? _result.size() : 0);\n")
.append(" System.out.println(\"[Method Exit] " + targetMethod + " returns: \" + _resultStr + \" size: \" + _size);\n")
.append("}");

这里刚好我是list返回的,就直接这么处理了, 我们通过一个简单的调用来验证下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public static void main(String[] args) throws Exception {
// 假设要监控 org.example.DemoService 类中的 queryList 方法
addMonitoringNew("org.example.DemoService", "queryList");
// 2. 使用自定义类加载器加载增强后的类
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name.equals("org.example.DemoService")) {
try {
// 读取增强后的类文件
byte[] bytes = java.nio.file.Files.readAllBytes(
java.nio.file.Paths.get("org/example/DemoService.class")
);
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
throw new ClassNotFoundException("Failed to load enhanced class", e);
}
}
return super.loadClass(name);
}
};

// 3. 加载增强后的类
Class<?> enhancedClass = classLoader.loadClass("org.example.DemoService");

// 4. 创建实例
Object instance = enhancedClass.newInstance();

// 5. 获取并调用方法
Method processMethod = enhancedClass.getMethod("queryList", String.class, String.class, String.class); // 假设方法接收一个String参数
Object result = processMethod.invoke(instance, "name1", "title1", "n3");

System.out.println("Method returned: " + result);
}
}
可以看到我们的输出结果
```java
Successfully enhanced method: queryList
[Method Entry] queryList params: name1, title1, n3
[Method Exit] queryList returns: [name1, title1] size: 2
Method returned: [name1, title1]

这里其实核心很依赖两个语法糖,类似这样的还有一些

1
2
3
4
5
6
7
8
9
10
11
12
$0, $1, $2, ...    	this and actual parameters
$args An array of parameters. The type of $args is Object[].
$$ All actual parameters.
For example, m($$) is equivalent to m($1,$2,...)

$cflow(...) cflow variable
$r The result type. It is used in a cast expression.
$w The wrapper type. It is used in a cast expression.
$_ The resulting value
$sig An array of java.lang.Class objects representing the formal parameter types.
$type A java.lang.Class object representing the formal result type.
$class A java.lang.Class object representing the class currently edited.

可以结合文档仔细学习下

0%