Flutter: Xcode error “Unable to boot the Simulator“

使用 flutter、Android Studio ,通过iOS模拟器运行项目,一直一切正常。

某次重启后无法启动模拟器,报错信息如下:

"Unable to boot the simulator".

解决方法,亲测有效:

macOS 14.4.1以及更高版本:
进入 “系统设置”→“通用”→“存储空间”→“开发者” 删除 “XCode 缓存” 。

在 macOS 13 及更高版本上:
进入 “系统设置”→“常规”→“存储”→“开发人员”
删除“开发者缓存”

在 macOS 12 及更低版本上:
转到“关于本机”→“存储”→“管理”→“开发人员”

参考链接


Flutter: XCode error “Unable to boot the Simulator

Podman安装最新的JFrog Artifactory制品仓库

1、前言

JFrog Artifactory 是目前全球唯一的支持所有开发语言,功能最强大的二进制制品仓库。

2、系统平台

操作系统 : Ubuntu 22.04.4 LTS (GNU/Linux 6.5.0-26-generic x86_64)

3、安装依赖&拉取镜像

使用 OSS 社区版本,官方有详细说明介绍

完整的执行指令如下图所示

# 安装 podman
$ sudo apt install podman

# 创建一个低权限用户,尽量不要使用root用户,身份操作podman,降低安全风险
$ sudo adduser podman
 
# 允许用户下的容器在系统启动的时候启动服务
$ sudo loginctl enable-linger podman

$ export JFROG_HOME=/home/podman/.dockers/jfrogoss
 
# 准备本地目录映射 
$ sudo mkdir -p $JFROG_HOME/var/etc/

$ cd $JFROG_HOME/var/etc/

$ sudo touch ./system.yaml

$ sudo chown -R podman $JFROG_HOME

$ sudo chown -R 1030:1030 $JFROG_HOME/var

$ sudo chmod -R 777 $JFROG_HOME/var

# 手工拉去部分镜像,或者可以在 /etc/containers/registries.conf 配置下载服务器,可省略前面的服务器 releases-docker.jfrog.io 
$ podman pull releases-docker.jfrog.io/jfrog/artifactory-oss

执行上面的指令后,可以看到 releases-docker.jfrog.io/jfrog/artifactory-oss 镜像下载进度。

4、启动配置

首先要创建本地的数据目录,然后启动的时候挂载本地创建的数据目录即可。

执行启动指令: 

# 为 Docker 容器分配内存资源。可以使用 --memory 参数设置容器可以使用的内存量,使用 --memory-swap 参数限制交换空间的大小

$ podman run --name jfrogoss --cpus=2 --memory=2g --memory-swap=4g -d -v /home/podman/.dockers/jfrogoss/var:/var/opt/jfrog/artifactory -p 18081:8081 -p 18082:8082 releases-docker.jfrog.io/jfrog/artifactory-oss

# 查看该容器
$ podman ps

# 如果需要进入容器查看执行情况,参考如下命令
# podman exec -it jfrogoss bash

# 每次都启动新容器方式创建servcie //--new参数,每次启动都删除旧容器,启动一个新容器 
$ podman generate systemd --restart-policy=always -n --new -f jfrogoss

查看进程及IP跟端口号,启动一切正常了,打开浏览器访问系统看看效果。

查看启动文件:

$ cat container-jfrogoss.service

内容如下:

# container-jfrogoss.service
# autogenerated by Podman 3.4.4
# Sat Mar 30 19:04:05 CST 2024

[Unit]
Description=Podman container-jfrogoss.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
ExecStartPre=/bin/rm -f %t/%n.ctr-id
ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon --replace --name jfrogoss --cpus=2 --memory=2g --memory-swap=4g -d -v /home/podman/.dockers/jfrogoss/var:/var/opt/jfrog/artifactory -p 18081:8081 -p 18082:8082 releases-docker.jfrog.io/jfrog/artifactory-oss
ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all

[Install]
WantedBy=default.target

需要额外注意的一个地方是,给出的路径必须是完整路径 “/home/podman/.dockers/jfrogoss”,不能是 “~/.dockers/jfrogoss”,Systemd不能正确展开 “~” ,导致路径找不到,从而在启动的时候失败,报告错误代码 125 。

Systemd 配置,开机/重启自动启动服务:

# 切换到刚刚创建的低权限用户 ,注意,必须使用 su - user 的方式切换
# 如果使用 su user 切换会导致环境变量被带到新用户,导致执行报错
# ERRO[0000] XDG_RUNTIME_DIR directory "/run/user/1000" is not owned by the current user 
 
$ su - podman

# 保存到 ~/.config/systemd/user/
$ mkdir .config

$ mkdir .config/systemd

$ mkdir .config/systemd/user

$ mv container-jfrogoss.service ~/.config/systemd/user/


# 以当前用户身份刷新配置文件,让其生效
$ systemctl --user daemon-reload

# 设置容器开机自启,并且现在启动
$ systemctl --user enable --now ~/.config/systemd/user/container-jfrogoss.service

# 如果需要进入容器查看执行情况,参考如下命令
# podman exec -it jfrogoss bash

# 测试,重启系统,观察是否能开机自动启动
$ sudo reboot

# 启动或重启服务
# systemctl --user start container-jfrogoss.service
# systemctl --user restart container-jfrogoss.service

# 如果启动失败,观察服务日志
# sudo journalctl -f

5、访问、登录

Access Artifactory from your browser at: 

http://SERVER_HOSTNAME:18082/ui/

For example, on your local machine: 

http://localhost:18082/ui/

默认用户名:admin, 密码: password

启用匿名访问:

Maven服务器配置

客户端配置 http://SERVER_HOSTNAME:18082/artifactory/maven-remote/

参考链接


修复 SecurityException: getDataNetworkTypeForSubscriber 问题

1.问题

  • 用户切换改变网络的过程中,应用概率会出现崩溃,日志如下

API level: '30'
OS version: '11'
ABI list: 'arm64-v8a,armeabi-v7a,armeabi'
Manufacturer: 'OnePlus'
Brand: 'OnePlus'
Model: 'GM1900'
Build fingerprint: 'OnePlus/OnePlus7_CH/OnePlus7:11/RKQ1.201022.002/2108161921:user/release-keys'
pid: 29126, tid: 29126, name: main  >>> package name <<<

java stacktrace:
java.lang.SecurityException: getDataNetworkTypeForSubscriber
    at android.os.Parcel.createExceptionOrNull(Parcel.java:2373)
    at android.os.Parcel.createException(Parcel.java:2357)
    at android.os.Parcel.readException(Parcel.java:2340)
    at android.os.Parcel.readException(Parcel.java:2282)
    at com.android.internal.telephony.ITelephony$Stub$Proxy.getNetworkTypeForSubscriber(ITelephony.java:8803)
    at android.telephony.TelephonyManager.getNetworkType(TelephonyManager.java:3070)
    at android.telephony.TelephonyManager.getNetworkType(TelephonyManager.java:3034)
    at j.k.b0.a.e.b.c(Unknown Source:51)
    at j.k.b.q.c.w.onCreateView(:14)
    at androidx.fragment.app.Fragment.performCreateView(Unknown Source:15)
    at h.l.d.q.V(:18)
    at h.l.d.q.T(:1)
    at h.l.d.q.U(Unknown Source:47)
    at h.l.d.a.n(Unknown Source:182)
    at h.l.d.q.E(:7)
    at h.l.d.q.b0(Unknown Source:84)
    at h.l.d.q.D(Unknown Source:31)
    at h.l.d.a.e(:2)
    at h.l.d.v.finishUpdate(Unknown Source:12)
    at androidx.viewpager.widget.ViewPager.r(:2)
    at androidx.viewpager.widget.ViewPager.onMeasure(:1)
    at android.view.View.measure(View.java:25671)
    at androidx.constraintlayout.widget.ConstraintLayout.e(:2)
    at androidx.constraintlayout.widget.ConstraintLayout.onMeasure(:70)
    at android.view.View.measure(View.java:25671)
    at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6987)
    at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
    at androidx.appcompat.widget.ContentFrameLayout.onMeasure(Unknown Source:154)
    at android.view.View.measure(View.java:25671)
    at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6987)
    at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1552)
    at android.widget.LinearLayout.measureVertical(LinearLayout.java:842)
    at android.widget.LinearLayout.onMeasure(LinearLayout.java:721)
    at android.view.View.measure(View.java:25671)
    at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6987)
    at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
    at android.view.View.measure(View.java:25671)
    at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6987)
    at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1552)
    at android.widget.LinearLayout.measureVertical(LinearLayout.java:842)
    at android.widget.LinearLayout.onMeasure(LinearLayout.java:721)
    at android.view.View.measure(View.java:25671)
    at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6987)
    at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
    at com.android.internal.policy.DecorView.onMeasure(DecorView.java:776)
    at android.view.View.measure(View.java:25671)
    at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:3713)
    at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:2496)
    at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2771)
    at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2182)
    at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8730)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1352)
    at android.view.Choreographer.doCallbacks(Choreographer.java:1149)
    at android.view.Choreographer.doFrame(Choreographer.java:1049)
    at android.view.Choreographer$FrameHandler.handleMessage(Choreographer.java:1275)
    at android.os.Handler.dispatchMessage(Handler.java:106)
    at android.os.Looper.loop(Looper.java:233)
    at android.app.ActivityThread.main(ActivityThread.java:8010)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:631)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:978)

另外一种场景就是集成了华为的二维码扫描  com.huawei.hms:scanplus:1.3.2.300 ,这个  SDK 比较奇葩的地方在于,使用 Wi-Fi 网络情况下是不会出现问题的。

但是在手机流量的情况下,不申请 android.permission.READ_PHONE_STATE 权限,初始化这个 SDK 会导致应用闪退。

崩溃堆栈如下:

java.lang.SecurityException: getNetworkTypeForSubscriber
	at android.os.Parcel.createExceptionOrNull(Parcel.java:2437)
	at android.os.Parcel.createException(Parcel.java:2421)
	at android.os.Parcel.readException(Parcel.java:2404)
	at android.os.Parcel.readException(Parcel.java:2346)
	at com.android.internal.telephony.ITelephony$Stub$Proxy.getNetworkTypeForSubscriber(ITelephony.java:9325)
	at android.telephony.TelephonyManager.getNetworkType(TelephonyManager.java:3002)
	at android.telephony.TelephonyManager.getNetworkType(TelephonyManager.java:2964)
	at com.huawei.hms.mlkit.common.ha.d.b(HianalyticsLogUtils.java:68)
	at com.huawei.hms.mlkit.common.ha.HianalyticsLogProvider.logEnd(HianalyticsLogProvider.java:6315)
	at com.huawei.hms.ml.camera.g$a.a(HiAnalyticsThread.java:109)
	at com.huawei.hms.ml.camera.g$a.handleMessage(HiAnalyticsThread.java:78)
	at android.os.Handler.dispatchMessage(Handler.java:114)
	at android.os.Looper.loopOnce(Looper.java:206)
	at android.os.Looper.loop(Looper.java:296)
	at com.huawei.hms.ml.camera.g.run(HiAnalyticsThread.java:51)

2.分析

  • 根据 SecurityException: getDataNetworkTypeForSubscriber 可以看到,这是一个安全性异常,所以猜测应用在 Android11 的权限有关,由于缺少该权限导致无法访问接口而异常。

  • 找到网络状态检测方法,可以看到调用了 TelephonyManager.getNetworkType()接口获取网络类型,该方法是需要 READ_PHONE_STATE 权限的,该方法上面也有 RequiresPermission 注解声明

    @RequiresPermission(value = "android.permission.READ_PHONE_STATE")
    public int getNetworkState(Context context) {
        if (null == mConnectivityManager) { // 为空则认为无网络
            return NETWORK_NONE;
        }
        // 获取网络类型,如果为空,返回无网络
        NetworkInfo activeNetInfo = mConnectivityManager.getActiveNetworkInfo();
        if (activeNetInfo == null || !activeNetInfo.isAvailable()) {
            return NETWORK_NONE;
        }
        // 判断是否为WIFI
        NetworkInfo wifiInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
        if (null != wifiInfo) {
            NetworkInfo.State state = wifiInfo.getState();
            if (null != state) {
                if (state == NetworkInfo.State.CONNECTED || state == NetworkInfo.State.CONNECTING) {
                    return NETWORK_WIFI;
                }
            }
        }
        // 若不是WIFI,则去判断是2G、3G、4G网
        TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
        int networkType = telephonyManager.getNetworkType();
        ...
    }
  • 进一步分析,接口的调用堆栈为:

TelephonyManager.getNetworkType ->PhoneInterfaceManager.getNetworkTypeForSubscriber->TelephonyPermissions.checkCallingOrSelfReadPhoneState->TelephonyPermissions.checkReadPhoneState->ContextImpl.enforcePermission->ContextImpl.checkPermission->PermissionManager.checkPermission->PermissionTelephonyManager.getNetworkType ->
PhoneInterfaceManager.getNetworkTypeForSubscriber->
TelephonyPermissions.checkCallingOrSelfReadPhoneState->
TelephonyPermissions.checkReadPhoneState->
ContextImpl.enforcePermission->
ContextImpl.checkPermission->
PermissionManager.checkPermission->
PermissionManager.checkPermissionUncached->
ActivityManager.getService->
IActivityManager.checkPermission->

3.解决方法

private void requestPermission() {
        LogUtil.printE(HomeActivity.class, ">>> etrunc requestPermission SDK-VERSION= " + Build.VERSION.SDK_INT);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { //Android 11 授权读写权限
            XXPermissions.with(HomeActivity.this)
                    // 不适配 Android 11 可以这样写
                    //.permission(Permission.Group.STORAGE)
                    // 适配 Android 11 需要这样写,这里无需再写 Permission.Group.STORAGE
                    .permission(Permission.MANAGE_EXTERNAL_STORAGE, Permission.READ_PHONE_STATE)
                    .request(new OnPermissionCallback() {

                        @Override
                        public void onGranted(List<String> permissions, boolean all) {
                            if (all) {
                                ToastUtil.showToast(getApplication(), R.string.permission_success_tip);
                            }
                        }

                        @Override
                        public void onDenied(List<String> permissions, boolean never) {
                            if (never) {
                                ToastUtil.showToast(getApplication(), R.string.permissions_refuse_authorization);
                                // 如果是被永久拒绝就跳转到应用权限系统设置页面
                                XXPermissions.startPermissionActivity(HomeActivity.this, permissions);
                            } else {
                                ToastUtil.showToast(getApplication(), R.string.permissions_error);
                            }
                        }
                    });
        }
    }

参考链接


修复 SecurityException: getDataNetworkTypeForSubscriber 问题

Linux使用SMB给macOS做无线Time Machine备份

前要:

一直以来使用外置硬盘给Mac做Time Machine备份盘,但是存在若干不够方便的地方,如:

  • 磁盘需要格式化为APFS格式,虽然APFS的“卷共享容器空间”的机制可以很方便的让Time Machine卷和其他资料卷共用空间,而不是像传统的分盘让空间分隔,还要考虑空间分配问题。但APFS格式只在macOS设备间方便使用
  • 虽然在不连接磁盘的情况下,内置存储也会保留24小时内到每小时快照,但总是需要刻意记起找出插入硬盘进行备份的操作

正好最近使用旧电脑刷了Ubuntu用作NAS使用,于是想了解关于如何配置无线Time Machine

无线Time Machine的共享协议选择:

最开始找到的教程是使用AFP的开源实现netatalk让Linux支持AFP共享协议,然后作为Time Machine盘。但发现netatalk最近曝出过严重漏洞项目本身在GitHub也只有0.2K Star的关注。主观感觉其稳定性是存在疑问的。

后来发现,并不一定是AFP协议的共享才能做Time Machine备份盘;Samba只要进行一些配置就能做Time Machine备份用了

在 Mac 上可以与时间机器配合使用的磁盘类型

在Apple官方文档中说明有写到

【提示】如果可以选择 SMB 或 AFP,请使用 SMB 来备份到外置备份磁盘。

目前Apple官方也是更推荐使用SMB协议来作为无线Time Machine备份的

Linux上Samba的配置

首先需要安装avahi和samba,关于其安装和配置和samba用户的设置管理这里不再赘述,不同的Linux发行版安装方式会有一些差异,可以自行搜索。

个人的 /etc/samba/smb.conf 内容如下

[global]
   workgroup = WORKGROUP
   server string = %h server (Samba, Ubuntu)
   log file = /var/log/samba/log.%m
   max log size = 1000
   logging = file
   panic action = /usr/share/samba/panic-action %d
   server role = standalone server
   obey pam restrictions = yes
   unix password sync = yes
   passwd program = /usr/bin/passwd %u
   passwd chat = *Enter\snew\s*\spassword:* %n\n *Retype\snew\s*\spassword:* %n\n *password\supdated\ssuccessfully* .
   pam password change = yes
   map to guest = never
   usershare allow guests = no
   security = user
   public = no
   writable = yes
   guest ok = no

   vfs objects = catia fruit streams_xattr
   min protocol = SMB3_11
   smb encrypt = required
   ea support = yes
   fruit:metadata = stream
   fruit:model = MacSamba
   fruit:posix_rename = yes
   fruit:veto_appledouble = no
   fruit:wipe_intentionally_left_blank_rfork = yes
   fruit:delete_empty_adfiles = yes
   fruit:aapl = yes
[samba_WD]
   #comment = mydisk
   #available = yes
   #browseable = yes
   path = /media/xiaoming/WD
   vaild user = xiaoming
   create mask = 0755
[samba_xiaoming]
   #comment = xiaominghome
   #available = yes
   #browseable = yes
   path = /home/xiaoming/Documents
   vaild user = xiaoming
   create mask = 0755
[myTimeMachine]
   path = /media/xiaoming/WD/tmbackup
   valid users = xiaoming
   fruit:time machine = yes
   #fruit:time machine max size = 500G

其中,与 Time Machine 的启用相关的关键部分为:

[global]
   vfs objects = catia fruit streams_xattr
   min protocol = SMB3_11
   smb encrypt = required
   ea support = yes
   fruit:metadata = stream
   fruit:model = MacSamba
   fruit:posix_rename = yes
   fruit:veto_appledouble = no
   fruit:wipe_intentionally_left_blank_rfork = yes
   fruit:delete_empty_adfiles = yes
   fruit:aapl = yes
[myTimeMachine]
   path = /media/xiaoming/WD/tmbackup
   valid users = xiaoming
   fruit:time machine = yes
   #fruit:time machine max size = 500G

因为个人这里是只配合自己的Mac使用的,所以配置文件尽量确保了安全性和与macOS设备的兼容性,比如在global中把最小连接协议设置为SMB3_11,强制启用了传输加密,和很多更加兼容macOS的fruit选项。请根据个人需求修改。

配置可参考

Configure Samba to Work Better with Mac OS X

最后使用systemctl restart smbd另配置生效即可。

备份盘的选择:

上述配置文件中,/media/xiaoming/WD是外置硬盘在fstab中设置挂载的路径,而Time Machine备份的目录则放在了该目录的./tmbackup下。登陆用户我设置为“xiaoming”,并确保这两个目录都具有用户“xiaoming”的权限读写。后续在使用SMB访问时,才能正常读写文件

特别提示:如果你的外置磁盘并非ext4这样的适合Linux使用的格式,而是如exFAT这种,会出现问题。像exFAT是不兼容在Linux上的文件用户分组的。个人测试,exFAT磁盘中所有文件所属只能为root:root。且使用sudo chown -R xiaoming:xiaoming也无法更改权限。建议备份磁盘文件后,格式化为ext4这种适合Linux使用的格式

macOS上的配置:

打开访达,边栏上应该能直接看到服务器,点击后点上面的“连接身份...”,再点“注册用户”,输入用户和密码,勾选“在我的钥匙串中记住此密码”,点连接,随后会显示出多个共享,多选连接即可。

如果边栏没有,则需按command+K来手动连接,输入smb://xiaomingnas.local。这里xiaomingnas.local中的xiaomingnas是主机的hostname,通常avahi的mDNS服务会使用<hostname>.local作为域名。点连接,后续步骤与上述相同。

随后在Mac的系统设置-通用-时间机器,点+号,就能选择设置的Time Machine进行备份了。然后根据提示设置登录用户名,时间机器的加密密码即可。

无线备份受限于网络传输速度,首次备份可能需要数小时才能完成,可耐心等待。

Time Machine默认设置了速度限制,以保障网络和磁盘可正常使用,首次备份可以在Mac上暂时解除该限制

#解除限制
$ sudo sysctl debug.lowpri_throttle_enabled=0

#恢复速度限制:
$ sudo sysctl debug.lowpri_throttle_enabled=1

#查看状态
$ sysctl -n debug.lowpri_throttle_enabled

用外置磁盘和无线共享设置Time Machine的区别:

使用外置磁盘备份,点击边栏上的卷后,可以看到目录形式展示的每次的备份,并直接查看其中的目录文件。还可以方便地多选备份进行删除,来腾出空间占用

而使用SMB共享作为备份盘。这会在共享路径下生成一个类似“小明的MacBook Air.sparsebundle”目录。大致结构如下

$ tree -L 2
.
└── 小明的MacBook Air.sparsebundle
    ├── bands
    ├── com.apple.TimeMachine.MachineID.bckup
    ├── com.apple.TimeMachine.MachineID.plist
    ├── com.apple.TimeMachine.Results.plist
    ├── com.apple.TimeMachine.SnapshotHistory.plist
    ├── Info.bckup
    ├── Info.plist
    ├── lock
    ├── mapped
    └── token

其中bands目录中会有大量固定大小为67MB的二进制文件,这些便是备份的数据。

Apple官方对sparsebundle的解释的大致意思是,它是一个以二进制形式存储的,可按需收缩和扩大的可扩展文件。

在 Mac 上使用“磁盘工具”创建磁盘映像

这样看用SMB共享设置Time Machine实际上更加灵活,相比之下不挑磁盘格式,且备份在Linux上的磁盘中仅作为某个路径下的某个目录存在,而不像使用外置磁盘备份一样单独占据一个APFS卷。

但其不能像外置磁盘一样,可以任意多选删除备份来腾出空间。但可以设置fruit:time machine max size来限制备份大小,这样达到空间临界点时macOS就会自动删除最旧的备份来腾出空间。当然,磁盘备份同样也可以在添加APFS卷时,通过设置“配额大小”来限制Time Machine过度膨胀(但设置完毕后不易变更)

实际使用的体验:

无线备份的速度慢于磁盘备份,这取决于你的局域网环境。不过备份过程同样是完全无感的,和有线备份一样,即使是合盖休眠情况下也会自动创建备份。

Time Machine是可以同时备份到多个位置的,因此可以同时使用磁盘备份和无线备份,这样数据安全性也更好。如果再配合zerotier异地组网,设置一个异地的NAS作为无线Time Machine,那么你就得到了一个符合“3-2-1原则”的,完全无感、自动化、无需干涉的备份系统。且Time Machine本身也有加密,无惧因备份介质遗失导致数据泄漏的风险。

考虑到传输速度,无线的Time Machine用于找回过去的文件没有问题,但不太适合做整机恢复,如果是换机这种操作,还是建议使用磁盘备份节省时间。

更建议使用 docker-timemachine 镜像进行配置,可以在搜索 docker-timemachine 然后进行配置。

参考链接


Linux使用SMB给macOS做无线Time Machine备份

商用密码技术最佳实践白皮书

密码算法库是操作系统的基础组件,在系统安全领域的作用不言而喻。操作系统默认已经内置了大量的密码学库,比如OpenSSL,libgcrypt,gnulib,nettle 是被默认集成到基础操作系统的,它们有一些重复的功能,但也各有侧重的领域,是操作系统不可或缺的安全基石。

本小节会介绍一些支持国密算法的、非常主流的密码算法库,提供给开发者和用户更多的选择。

OpenSSL

官网:https://www.openssl.org

OpenSSL 是一个通用的、强大的、商业级的、功能齐全的工具包,用于通用加密和安全通信。

OpenSSL 的重要性众所周知,这里重点强调一下版本问题。

🟢 1.1.1 稳定版

目前主流发行版使用的仍然是 1.1.1 版本,这个版本在国密的支持上有一些固有的缺陷:

  • 不支持 SM2 的签名验签,因为基于可辨别用户ID的Za值计算在这个版本中未实现
  • 国外主流的发行版的包默认没有编译国密 SM2、SM4 模块,CentOS上就是如此

由于技术上和兼容性的原因,这个版本目前很难升级到最新的社区版本,因此在主流的发行版本中基本是无缘使用国密的。

🟢 3.0.x 稳定版

社区最新的稳定版本是 3.0,这个版本对国密的支持已经比较完善,并且支持了国密的指令集优化。用户如果自行编译可以完整使能国密的能力。

🟢 龙蜥社区 1.1.1 版本

从目前情况来看,对于一个操作系统发行版,要完全从 1.1.1 切换到 3.0 还需要较长的时间,因此龙蜥社区在 1.1.1 版本的基础上,在保证兼容性和稳定性的前提下,补全了国密能力上的缺陷,并且做为操作系统默认库在 Anolis OS 8.8 中集成发布,详细信息可参考Anolis OS 国密开发指南

libgcrypt

官网:https://www.gnupg.org/software/libgcrypt/index.html

不像 OpenSSL 还包括了安全协议,libgcrypt 是一个纯粹的密码算法库,就国密算法的性能来说,libgcrypt 的国密算法优化是做的比较充分的,Linux 内核国密算法的部分优化也是先在 libgcrypt 实现后才移植到内核的。

Libgcrypt 是一个通用密码库,最初基于 GnuPG 的代码。 它为几乎所有的密码提供支持:

  • 对称密码算法 (AES、Arcfour、Blowfish、Camellia、CAST5、ChaCha20 DES、GOST28147、Salsa20、SEED、Serpent、Twofish、SM4)
  • 模式 (ECB、CFB、CBC、OFB、CTR、CCM) ,GCM,OCB,POLY1305,AESWRAP)
  • 哈希算法 (MD2, MD4, MD5, GOST R 34.11, RIPE-MD160, SHA-1, SHA2-224, SHA2-256, SHA2-384, SHA2-512, SHA3-224 , SHA3-256, SHA3-384, SHA3-512, SHAKE-128, SHAKE-256, TIGER-192, Whirlpool, SM3)
  • MAC (HMAC 用于所有哈希算法, CMAC 用于所有密码算法, GMAC-AES, GMAC-CAMELLIA, GMAC-TWOFISH、GMAC-SERPENT、GMAC-SEED、Poly1305、Poly1305-AES、Poly1305-CAMELLIA、Poly1305-TWOFISH、Poly1305-SERPENT、Poly1305-SEED)
  • 公钥算法 (RSA、Elgamal、DSA、ECDSA、EdDSA、ECDH、SM2)
  • 大整数函数、随机数和大量的支持函数

libgcrypt 是很多基础组件依赖的密码库,比如 gpg,systemd,qemu,postgresql,还有许多桌面环境的库,音视频组件,蓝牙都依赖于 libgcrypt 提供的密码安全机制,还有部分会选择依赖libgcrypt,比如 curl,cryptsetup 等会选择依赖 OpenSSL,libgcrypt 算法库,用户需要自行构建来选择不同的密码库。

libgcrypt 从 1.9.0 版本开始陆续支持了国密算法和国密的指令集优化。

GmSSL

项目地址:https://github.com/guanzhi/GmSSL

GmSSL 是一个开源密码工具包,为 GM/T 系列标准中规定的中国国家密码算法和协议提供一级支持。 作为 OpenSSL 项目的一个分支,GmSSL 提供了与 OpenSSL 的 API 级兼容性并保持了所有的功能。 现有项目(例如 Apache Web 服务器)可以轻松地移植到 GmSSL,只需进行少量修改和简单的重建。

自2014年底首次发布以来,GmSSL已入选开源中国六大推荐密码项目之一,并获得2015年中国Linux软件大奖。

该密码库的特点:

  • 支持中国GM/T密码标准。
  • 支持中国厂商的硬件密码模块。
  • 具有商业友好的开源许可证。
  • 由北京大学密码学研究组维护。

GmSSL 将支持以下所有 GM/T 加密算法:

  • SM3 (GM/T 0004-2012):具有 256 位摘要长度的密码哈希函数。
  • SM4(GM/T 0002-2012):密钥长度为128位,块大小为128位的块密码,也称为SMS4。
  • SM2(GM/T 0003-2012):椭圆曲线密码方案,包括数字签名方案、公钥加密、(认证)密钥交换协议和一种推荐的256位素数域曲线sm2p256v1。
  • SM9(GM/T 0044-2016):基于配对的密码方案,包括基于身份的数字签名、加密、(认证)密钥交换协议和一条256位推荐BN曲线。
  • ZUC(GM/T 0001-2012):流密码,采用128-EEA3加密算法和128-EIA3完整性算法。
  • SM1和SSF33:密钥长度为128位,块大小为128位的块密码,没有公开说明,只随芯片提供。

GmSSL 支持许多有用的加密算法和方案:

  • 公钥方案:Paillier、ECIES(椭圆曲线集成加密方案)
  • 基于配对的密码学:BF-IBE、BB1-IBE
  • 块密码和模式:Serpent、Speck
  • 块密码模式:FPE(格式保护加密)
  • 基于SM3/SM4的OTP(一次性密码)(GM/T 0021-2012)
  • 编码:Base58

ECDSA、RSA、AES、SHA-1 等 OpenSSL 算法在 GmSSL 中仍然可用。

nettle

官网:http://www.lysator.liu.se/~nisse/nettle

Nettle 是一个相对低层的加密库,旨在轻松适应各种工具包和应用程序。它开始于2001年的lsh的低级加密函数的集合。自2009年6月以来,Nettle 成为 GNU 软件包。

Nettle 的定位跟 libgcrypt 有点类似,是很多基础组件选择依赖的一个密码学库。

从提供的 API 上来看,Nettle 没有对算法做更高层次的抽象,每个不同的算法都有一套更易理解的接口,开发者也会更容易上手。

Nettle 从 3.8 版本开始支持了 SM3 算法,最新的开发分支已经合入了 SM4 算法,会在下一个 release 版本发布。

gnulib

官网:https://www.gnu.org/software/gnulib

从名字可以看出,gnulib 并不是一个纯密码算法的库,它的定位是 GNU 的公共代码库,旨在 GNU 包的源代码级别之间共享。

之所以在这里提 gnulib,是因为这个库里面实现了常用的哈希算法,也包括SM3算法,gnulib 里的 哈希算法主要是为 coreutils 包里的 sha*sum, md5sum 系列工具提供支持的,当然开发者也可以基于 gnulib 构建自己的程序。

gnulib 是在 2017 年 10 月支持了 SM3 算法,由阿里巴巴张佳贡献。

coreutils

coreutils 支持了大量的计算哈希的工具,比如 cksum,md5sum,b2sum,sha*sum 等,这些工具是紧密依赖于 gnulib 库的。

2017 年 10 月,在 gnulib 库支持了 SM3 之后,我们便向 coreutils 社区提交了 sm3sum 工具的支持,coreutils 社区却迟迟不愿接收,因为 SM3 算法的IV向量没有明确的来历说明,社区对算法的安全性有质疑,虽然彼时 SM3 已经是 ISO 的国际标准算法。社区人员认为 SM3 在 gnulib 中作为库提供给开发者是没有问题的,因为开发者具备也应该具备判断一个算法是否安全的能力,但是在 coreutils 中提供一个 sm3sum 的工具提供给终端用户会引起用户的误导,用户可能误认为算法安全性是得到保证的,尤其是在 SM3 算法安全性被质疑的前提下。

直到四年后的 2021 年 9 月,在包括Linux 内核,libgcrypt,OpenSSL 等主流的密码算法社区都支持了SM3算法后,在龙蜥的几次推动下,coreutils 社区终于不再质疑 SM3 的安全性问题,但是社区也不愿意再多引入一个工具,应该把这个哈希算法整合为一个工具,因为类似 *sum 的工具太多了。

因此,社区提出一个 cksum -a [algo] 的方案,通过给 cksum 工具添加一个算法参数,整合了目前 coreutils 中支持的所有哈希算法,为了兼容考虑,之前的 *sum 工具也继续保留了,SM3 是唯一仅在 cksum 工具中支持的算法,当然这并不是优点,使用习惯上也会有一些差异,用户需要通过 cksum -a sm3 来计算 SM3 哈希,除这个区别外,其它用法跟 md5sum 类似。

coreutils 从 9.0 版本开始支持 SM3 的哈希计算。

RustCrypto

这是一个纯 Rust 编写的密码算法库,供 Rust 开发者使用。

该项目维护着数十个流行的 crate,都提供密码算法的纯 Rust 实现,主要包括以下算法:

  • 非对称加密:椭圆曲线、rsa
  • 加密编码格式:const-oid、der、pem-rfc7468、pkcs8
  • 数字签名:dsa、ecdsa、ed25519、rsa
  • 椭圆曲线:k256、p256、p384
  • 哈希函数:blake2、sha2、sha3、sm3
  • 密钥派生函数:hkdf、pbkdf2
  • 消息认证码:hmac
  • 密码哈希:argon2、pbkdf2、scrypt
  • Sponge 函数:ascon、keccak
  • 对称加密:aes-gcm、aes-gcm-siv、chacha20poly1305、sm4
  • Traits:aead、密码、摘要、密码哈希、签名

该算法库目前支持 SM3 和 SM4 算法。

Intel IPP

项目地址:https://github.com/intel/ipp-crypto

Intel Integrated Performance Primitives (Intel IPP) Cryptography 是一个安全、快速且轻量级的密码学库,针对各种 Intel CPU 进行了高度优化。

该库提供了一套全面的常用于加密操作的函数,包括:

  • 对称密码学原语函数:

    • AES(ECB、CBC、CTR、OFB、CFB、XTS、GCM、CCM、SIV)
    • SM4(ECB、CBC、CTR、OFB、CFB、CCM)
    • TDES(ECB、CBC、CTR、OFB、CFB)
    • RC4
  • 单向哈希原语:

    • SHA-1、SHA-224、SHA-256、SHA-384、SHA-512
    • MD5
    • SM3
  • 数据认证原语函数:

    • HMAC
    • AES-CMAC
  • 公钥加密函数:

    • RSA、RSA-OAEP、RSA-PKCS_v15、RSA-PSS
    • DLP、DLP-DSA、DLP-DH
    • ECC(NIST 曲线)、ECDSA、ECDH、EC-SM2
  • 多缓冲区 RSA、ECDSA、SM3、x25519

  • 有限域算术函数

  • 大整数算术函数

  • PRNG/TRNG 和质数生成

使用英特尔 IPP 密码库的原因:

  • 安全性(秘密处理功能的恒定时间执行)
  • 专为小尺寸设计
  • 针对不同的 Intel CPU 和指令集架构进行了优化(包括硬件加密指令 SSE 和 AVX 的支持)
  • 可配置的 CPU 分配以获得最佳性能
  • 内核模式兼容性
  • 线程安全设计

参考链接


关于Ubuntu下ZRAM的配置和使用

当前自己使用的服务器内存比较小,需要扩容,只是服务器比较老了,内存扩展困难,但是服务器的负载并不高。因此可以尝试使用内存压缩的方式来适当扩展内存。

安装

软件包封装好了对应的东西(甚至是systemctl服务),并不需要自己写脚本

$ sudo apt install zram-config

重启之后,一个内存大小一半的 ZRAM 就出现了。

参考链接


HarmonyOS NEXT鸿蒙手机Charles/Reqable抓包证书配置方面的一些疑惑

【问题1】

鸿蒙手机配置网络代理,只能断开wifi连接,然后再重新连接时才能配置代理?是这样的吗?华为设备网络代理配置需要长按对应的wifi配置弹出,但鸿蒙手机中貌似不管用。

解答

是的。当前确实需要断开WIFI再重新连接时配置代理。

【问题2】

鸿蒙的证书导入有什么推荐的方案吗?使用访问 chls.pro/ssl 的方式不会自动下载?下载证书后,使用「华为管家」,但 Mac 好像没有一个比较稳定的版本?

解答

首先,Mac PCCharles 导出证书,点击 Help -> SSL Proxying -> Save Charles Root Certificate

其次,导入系统根证书至手机,有两个方法。

方法一:启动证书安装器进行指定 pem 证书安装。

1.将 Charles 导出的 pem 文件 hdc file send 到手机存储器内。

2.启动动证书安装

$ hdc shell aa start -a MainAbility -b com.ohos.certmanager

3.选择从存储设备安装,选择指定 pem 证书。

方法二:替换 CA 证书,证书路径为沙箱映射路径,系统预设 CA 证书位置:/etc/ssl/certs/cacert.pem,将 Charles 导出的 pem 文件 hdc file send 到此路径下(目前仅支持后缀名为 .pem 的文本格式证书)。

示例命令:

//重新挂载根目录为可写
$ hdc shell mount -o remount,rw /
//导入根证书
$ hdc file send ./cacert.pem /etc/ssl/certs/

再次,安装 Charles 证书到 PC 系统可信目录。

点击 Help -> SSL Proxying -> Install Charles Root Certificate -> 安装证书 -> 选择证书存储路径为:受信任的根证书颁发机构。

最后,设置代理。

点击 Proxy -> SSL Proxy Settings -> 在 Include 添加 *:**:443

点击 Proxy -> Proxy Settings  -> 勾选 Enable transparent HTTP proxying

注意:截止 2024/03/25 ,华为P60 升级到的鸿蒙 HarmonyOS NEXT,按照上述方式配置之后,依旧是无法通过 Charles / Reqable 进行中间人代理的。目前看到系统并没有使用我们刚刚导入的根证书。导致无法完成中间人抓包。

目前可以部分解决的问题方式是自己启动一个 Tomcat 服务器,然后配置客户端通过 HTTP 的方式进行报文的发送,从而可以看到上行报文内容。

【问题3】

鸿蒙的证书信任如何设置?设置中搜索一些配置关键字然后信任,但问题是,我的鸿蒙测试设备的设置中根本没有搜索……

解答

Charles 导出的 pem 文件 hdc file send 到手机存储器内。

可以参考如下命令:

$ hdc file send charles.pem(pc上证书路径) /storage/media/100/local/files/Download/(工程机指定路径)

注意:截止 2024/03/25 ,华为P60 升级到的鸿蒙 HarmonyOS NEXT,只能通过 hdc file send 发送到手机的临时目录 temp 路径下,而证书凭证应用并不能找到这个目录,导致依旧无法导入证书。

目前可以解决的问题方式是通过U盘作为中介的方式完成文件的传输。

参考链接


Ubuntu 22.04使用Podman部署OpenGrok的详细教程

安装必要的依赖:

# 安装 podman
$ sudo apt install podman

# 安装 git
$ sudo apt install git

# 创建一个低权限用户,尽量不要使用root用户,身份操作podman,降低安全风险
$ sudo adduser podman

# 允许用户下的容器在系统启动的时候启动服务
$ sudo loginctl enable-linger podman

# 切换到刚刚创建的低权限用户 ,注意,必须使用 su - user 的方式切换
# 如果使用 su user 切换会导致环境变量被带到新用户,导致执行报错
# ERRO[0000] XDG_RUNTIME_DIR directory "/run/user/1000" is not owned by the current user 

$ su - podman

# 准备本地目录映射
$ mkdir ~/.dockers

$ mkdir ~/.dockers/opengrok

# 准备已经处理过的源代码,源代码需要 src、etc、data 三个目录,src存储源代码,其他目录保持空即可
# 参考 https://www.mobibrw.com/2019/19162
# 更详细的处理参考官方文档即可,后续我们以 Android 4.2.2 的源代码为例子

# Android 4.2.2 源代码存放在 ~/.dockers/opengrok/Android_4.2.2/src
$ mkdir ~/.dockers/opengrok/Android_4.2.2/src

$ mkdir ~/.dockers/opengrok/Android_4.2.2/etc

$ mkdir ~/.dockers/opengrok/Android_4.2.2/data

# 手工拉去部分镜像,或者可以在 /etc/containers/registries.conf 配置镜像服务器,可省略前面的服务器 docker.io
$ podman pull docker.io/library/tomcat:10-jdk17

官方镜像会在报错的时候暴露 Tomcat 10 版本号,错误堆栈,构成安全隐患,我们需要通过构建自定义镜像解决此问题:

# 目前最新代码 commit ad9658443340a6dae425d94ab7cb0cc9d74fb37f
$ git clone https://github.com/oracle/opengrok.git

$ cd opengrok

# 设置 maven 国内代理,否则下载速度太慢了 
$ find . -type f -exec grep -l "repo.maven" {} \; | xargs sed -i "s/repo.maven.apache.org\/maven2\//mirrors.cloud.tencent.com\/nexus\/repository\/maven-public\//"

# 设置 Maven Wrapper 国内源
$ sed -i "s/repo.maven.apache.org\/maven2\//mirrors.cloud.tencent.com\/nexus\/repository\/maven-public\//" .mvn/wrapper/maven-wrapper.properties

# 代码中使用的 linux 系统镜像版本,我们需要明确告知 podman 从 docker.io 获取
$ sed -i "s/ubuntu:jammy/docker.io\/ubuntu:jammy/g" Dockerfile

# 代码中使用的 Tomcat 镜像版本,不能通过 docker.io 获取到,我们直接使用通用 Tomcat 10 版本
$ sed -i "s/tomcat:10.1.19-jdk17/docker.io\/library\/tomcat:10.1-jdk17/g" Dockerfile

内容如下:

# Copyright (c) 2018, 2021 Oracle and/or its affiliates. All rights reserved.
# Portions Copyright (c) 2020, Chris Fraire <cfraire@me.com>.

FROM ubuntu:jammy AS build

# hadolint ignore=DL3008
RUN apt-get update && apt-get install --no-install-recommends -y openjdk-17-jdk python3 python3-venv && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Create a first layer to cache the "Maven World" in the local repository.
# Incremental docker builds will always resume after that, unless you update the pom
WORKDIR /mvn
COPY pom.xml /mvn/
COPY mvnw /mvn/
COPY .mvn /mvn/.mvn
COPY opengrok-indexer/pom.xml /mvn/opengrok-indexer/
COPY opengrok-web/pom.xml /mvn/opengrok-web/
COPY plugins/pom.xml /mvn/plugins/
COPY suggester/pom.xml /mvn/suggester/

# distribution and tools do not have dependencies to cache
RUN sed -i 's:<module>distribution</module>::g' /mvn/pom.xml && \
    sed -i 's:<module>tools</module>::g' /mvn/pom.xml && \
    mkdir -p /mvn/opengrok-indexer/target/jflex-sources && \
    mkdir -p /mvn/opengrok-web/src/main/webapp/js && \
    mkdir -p /mvn/opengrok-web/src/main/webapp/WEB-INF/ && \
    touch /mvn/opengrok-web/src/main/webapp/WEB-INF/web.xml

# dummy build to cache the dependencies
RUN ./mvnw -DskipTests -Dcheckstyle.skip -Dmaven.antrun.skip package

# build the project
COPY ./ /opengrok-source
WORKDIR /opengrok-source

RUN /mvn/mvnw -DskipTests=true -Dmaven.javadoc.skip=true -B -V package
# hadolint ignore=SC2012,DL4006
RUN cp `ls -t distribution/target/*.tar.gz | head -1` /opengrok.tar.gz

# Store the version in a file so that the tools can report it.
RUN /mvn/mvnw help:evaluate -Dexpression=project.version -q -DforceStdout > /mvn/VERSION

FROM tomcat:10-jdk17
LABEL maintainer="https://github.com/oracle/opengrok"

# Add Perforce apt source.
# hadolint ignore=DL3008,DL3009
RUN apt-get update && \
    apt-get install --no-install-recommends -y gnupg2
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# hadolint ignore=DL3059
RUN curl -sS https://package.perforce.com/perforce.pubkey | gpg --dearmor > /etc/apt/trusted.gpg.d/perforce.gpg
# hadolint ignore=DL3059
RUN echo 'deb https://package.perforce.com/apt/ubuntu jammy release' > /etc/apt/sources.list.d/perforce.list

# install dependencies and Python tools
# hadolint ignore=DL3008,DL3009
RUN apt-get update && \
    apt-get install --no-install-recommends -y git subversion mercurial cvs cssc bzr rcs rcs-blame \
    unzip python3 python3-pip \
    python3-venv python3-setuptools openssh-client libyaml-dev

# hadolint ignore=DL3008,DL3059
RUN architecture=$(uname -m) && if [[ "$architecture" == "aarch64" ]]; then \
        echo "aarch64: do not install helix-p4d."; else \
        apt-get install --no-install-recommends -y helix-p4d; fi

# compile and install universal-ctags
# hadolint ignore=DL3003,DL3008
RUN apt-get install --no-install-recommends -y pkg-config automake build-essential && \
    git clone https://github.com/universal-ctags/ctags /root/ctags && \
    cd /root/ctags && ./autogen.sh && ./configure && make && make install && \
    apt-get remove -y automake build-essential && \
    apt-get -y autoremove && apt-get -y autoclean && \
    cd /root && rm -rf /root/ctags && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# 设置国内源,否则大概率不成功
RUN python3 -m pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

# Update the Python tooling in order to successfully install the opengrok-tools package.
# hadolint ignore=DL3013
RUN python3 -m pip install --no-cache-dir --upgrade pip setuptools

# prepare OpenGrok binaries and directories
# hadolint ignore=DL3010
COPY --from=build opengrok.tar.gz /opengrok.tar.gz
# hadolint ignore=DL3013
RUN mkdir -p /opengrok /opengrok/etc /opengrok/data /opengrok/src && \
    tar -zxvf /opengrok.tar.gz -C /opengrok --strip-components 1 && \
    rm -f /opengrok.tar.gz && \
    python3 -m pip install --no-cache-dir /opengrok/tools/opengrok-tools.tar.gz && \
    python3 -m pip install --no-cache-dir Flask Flask-HTTPAuth waitress # for /reindex REST endpoint handled by start.py

COPY --from=build /mvn/VERSION /opengrok/VERSION

# environment variables
ENV SRC_ROOT /opengrok/src
ENV DATA_ROOT /opengrok/data
ENV URL_ROOT /
ENV CATALINA_HOME /usr/local/tomcat
ENV CATALINA_BASE /usr/local/tomcat
ENV CATALINA_TMPDIR /usr/local/tomcat/temp
ENV PATH $CATALINA_HOME/bin:$PATH
ENV CLASSPATH /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar
ENV JAVA_OPTS="--add-exports=java.base/jdk.internal.ref=ALL-UNNAMED --add-exports=java.base/sun.nio.ch=ALL-UNNAMED \
--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED \
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED \
--add-opens=java.base/java.util=ALL-UNNAMED"

# disable all file logging
COPY docker/logging.properties /usr/local/tomcat/conf/logging.properties
RUN sed -i -e 's/Valve/Disabled/' /usr/local/tomcat/conf/server.xml

# add our scripts and configuration
COPY docker /scripts
RUN chmod -R +x /scripts

# 关闭报错详情,这个会导致安全风险
RUN sed -i "s/<\/Host>/  <Valve className='org.apache.catalina.valves.ErrorReportValve' showReport='false' showServerInfo='false' \/>\n\t<\/Host>/" /usr/local/tomcat/conf/server.xml
 
# 配置AJP协议
# 如果只希望通过 AJP访问,可以参考如下命令 移除原有的 AJP 协议配置
# RUN xmlstarlet ed -L -P -S -d '/Server/Service/Connector' /usr/local/tomcat/conf/server.xml
 
# 增加新的协议配置
RUN sed -i "s/<\/Service>/  <Connector port='8009' protocol='AJP\/1.3' address='0.0.0.0' redirectPort='8443' secretRequired=''\/>\n <\/Service>/" /usr/local/tomcat/conf/server.xml

# run
WORKDIR $CATALINA_HOME
EXPOSE 8080
CMD ["/scripts/start.py"]

构建镜像:

$ podman --cgroup-manager=cgroupfs build -t opengrok-dev .

设置容器:

$ podman stop android-4.2.2

$ podman rm android-4.2.2

# 启动一个 tomcat 容器 8081 HTTP 访问端口 8010 AJP访问端口
# 注意,Docker 镜像会自动 每隔10分钟进行一次自动索引重建,不需要手动进行索引操作,
# 如果需要进行反向代理,则 OPENGROK_WEBAPP_CONTEXT 环境变量是无效的,
# Docker 需要使用 URL_ROOT 进行配置
# 详细配置信息参考源代码目录下的 docker/README.md
# 配置 SYNC_PERIOD_MINUTES=0 禁止周期性重建索引,但是容器重启的时候仍然会自动重建
# 多个容器的情况下,需要限制容器使用的CPU、内存资源,否则会导致宿主机内存不足,这个需要特别注意
# 这里我们分配两个核心给容器,一般限制核心也可以限制并发的Java进程,从而间接可以限制内存占用
# 同时应该限制容器内的同步线程数量 "WORKERS=1",否则极易引起宿主机的OOM 
# podman run -d --cpus=2 --name opengrok-android-4.2.2 -p 8081:8080/tcp -p 8010:8009 -v ~/opengrok-src/:/opengrok/src/ -v ~/opengrok-etc/:/opengrok/etc/ -v ~/opengrok-data/:/opengrok/data/ -e "URL_ROOT=Android_4.2.2" -e "SYNC_PERIOD_MINUTES=0" -e "WORKERS=1" localhost/opengrok-dev

$ podman run -d --cpus=2 --name opengrok-android-4.2.2 -p 8081:8080/tcp -p 8010:8009 -v /home/podman/.dockers/opengrok/Android_4.2.2/src:/opengrok/src/ -v /home/podman/.dockers/opengrok/Android_4.2.2/etc/:/opengrok/etc/ -v /home/podman/.dockers/opengrok/Android_4.2.2/data/:/opengrok/data/ -e "URL_ROOT=Android_4.2.2" -e "SYNC_PERIOD_MINUTES=0" -e "WORKERS=1" localhost/opengrok-dev

# 查看该容器
$ podman ps

# 如果需要进入容器查看执行情况,参考如下命令
# podman exec -it opengrok-android-4.2.2 bash

# 查看日志
# podman logs -f -t opengrok-android-4.2.2

# 每次都启动新容器方式创建servcie //--new参数,每次启动都删除旧容器,启动一个新容器 
$ podman generate systemd --restart-policy=always -n --new -f android-4.2.2

查看启动文件:

$ cat container-opengrok-android-4.2.2.service

内容如下:

# container-opengrok-android-4.2.2.service
# autogenerated by Podman 3.4.4
# Fri Mar 22 10:41:54 CST 2024

[Unit]
Description=Podman container-opengrok-android-4.2.2.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
ExecStartPre=/bin/rm -f %t/%n.ctr-id
ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon --replace -d --cpus=2 --name opengrok-android-4.2.2 -p 8081:8080/tcp -p 8010:8009 -v /home/podman/.dockers/opengrok/Android_4.2.2/src:/opengrok/src/ -v /home/podman/.dockers/opengrok/Android_4.2.2/etc/:/opengrok/etc/ -v /home/podman/.dockers/opengrok/Android_4.2.2/data/:/opengrok/data/ -e "URL_ROOT=Android_4.2.2" -e "SYNC_PERIOD_MINUTES=0" -e "WORKERS=1" localhost/opengrok-dev
ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all

[Install]
WantedBy=default.target

需要额外注意的一个地方是,给出的路径必须是完整路径 “/home/podman/.dockers/opengrok/Android_4.2.2/src”,不能是 “~/.dockers/opengrok/Android_4.2.2/src”,Systemd不能正确展开 “~” ,导致路径找不到,从而在启动的时候失败,报告错误代码 125 。

另外,Android源代码目录下不能存在 .svn .git  隐藏子目,我们需要手工删除,否则会报错。参考 删除目录下所有的 .svn .git 隐藏子目

Systemd 配置,开机/重启自动启动服务:

# 保存到 ~/.config/systemd/user/
$ mkdir .config

$ mkdir .config/systemd

$ mkdir .config/systemd/user

$ mv container-opengrok-android-4.2.2.service ~/.config/systemd/user/


# 以当前用户身份刷新配置文件,让其生效
$ systemctl --user daemon-reload

# 设置容器开机自启,并且现在启动
$ systemctl --user enable --now ~/.config/systemd/user/container-opengrok-android-4.2.2.service

# 如果需要进入容器查看执行情况,参考如下命令
# podman exec -it opengrok-android-4.2.2 bash

# 查看日志
# podman logs -f -t opengrok-android-4.2.2

# 测试,重启系统,观察是否能开机自动启动
$ sudo reboot

# 启动或重启服务 
# systemctl --user start container-opengrok-android-4.2.2.service 
# systemctl --user restart container-opengrok-android-4.2.2.service

# 如果启动失败,观察服务日志
# sudo journalctl -f
Running containers with resource limits fails with a permissions error

On some systemd-based systems, non-root users do not have resource limit delegation permissions. This causes setting resource limits to fail.

Symptom

Running a container with a resource limit options will fail with an error similar to the following:

--cpus--cpu-period--cpu-quota--cpu-shares:

Error: OCI runtime error: crun: the requested cgroup controller `cpu` is not available

--cpuset-cpus--cpuset-mems:

Error: OCI runtime error: crun: the requested cgroup controller `cpuset` is not available

This means that resource limit delegation is not enabled for the current user.

Solution

You can verify whether resource limit delegation is enabled by running the following command:

$ cat "/sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/cgroup.controllers"

Example output might be:

memory pids

In the above example, cpu and cpuset are not listed, which means the current user does not have permission to set CPU or CPUSET limits.

If you want to enable CPU or CPUSET limit delegation for all users, you can create the file /etc/systemd/system/user@.service.d/delegate.conf with the contents:

[Service] 
Delegate=memory pids cpu cpuset

After logging out and logging back in, you should have permission to set CPU and CPUSET limits.

参考链接


Ubuntu 22.04使用Podman部署Tomcat 10的详细教程

安装必要的依赖:

# 安装 podman
$ sudo apt install podman

# 创建一个低权限用户,尽量不要使用root用户,身份操作podman,降低安全风险
$ sudo adduser podman

# 允许用户下的容器在系统启动的时候启动服务
$ sudo loginctl enable-linger podman

# 退出当前用户,切换到刚刚创建的低权限用户, 注意,不要使用 su - user 的方式切换,后续会执行各种报错
# 如果使用 su user 切换会导致环境变量被带到新用户,导致执行报错
# ERRO[0000] XDG_RUNTIME_DIR directory "/run/user/1000" is not owned by the current user 
# Error running systemd as user - Failed to connect to bus: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined

$ exit

# 通过 ssh 登录用户名 podman 密码是创建用户的时候设置的密码

# 准备本地目录映射
$ mkdir ~/.dockers/tomcat

$ mkdir ~/.dockers/tomcat/webapps

$ mkdir ~/.dockers/tomcat/logs

官方镜像会在报错的时候暴露 Tomcat 10 版本号,错误堆栈,构成安全隐患,我们需要通过构建自定义镜像解决此问题:

$ touch Dockerfile

内容如下:

# 详细启动日志在 /usr/local/tomcat/logs 目录下

From docker.io/library/tomcat:10

# xml 编辑工具,方便我们后续修改 tomcat 的xml配置文件
# RUN apt update && apt install -y xmlstarlet

# 增加 <Valve className="org.apache.catalina.valves.ErrorReportValve" showReport="false" showServerInfo="false" /> 阻止错误日志输出

RUN sed -i "s/<\/Host>/  <Valve className='org.apache.catalina.valves.ErrorReportValve' showReport='false' showServerInfo='false' \/>\n\t<\/Host>/" /usr/local/tomcat/conf/server.xml

# 配置AJP协议
# 如果只希望通过 AJP访问,可以参考如下命令 移除原有的 AJP 协议配置
# RUN xmlstarlet ed -L -P -S -d '/Server/Service/Connector' /usr/local/tomcat/conf/server.xml

# 增加新的协议配置
RUN sed -i "s/<\/Service>/  <Connector port='8009' protocol='AJP\/1.3' address='0.0.0.0' redirectPort='8443' secretRequired=''\/>\n  <\/Service>/" /usr/local/tomcat/conf/server.xml

# 移除 xml 编辑工具
# RUN apt -y autoremove --purge xmlstarlet

构建镜像:

$ podman --cgroup-manager=cgroupfs build -t tomcat-10 .

设置容器开机自启:

$ podman stop tomcat-10

$ podman rm tomcat-10

# 启动一个 tomcat 容器 8080 HTTP 访问端口 8009 AJP访问端口
# podman run -d --name tomcat-10 -p 8080:8080 -p 8009:8009 -v /my/local/path:/usr/local/tomcat/webapps localhost/tomcat-10

$ podman run -d --name tomcat-10 -p 8080:8080 -p 8009:8009 -v ~/.dockers/tomcat/webapps:/usr/local/tomcat/webapps -v ~/.dockers/tomcat/logs:/usr/local/tomcat/logs localhost/tomcat-10

# 查看该容器
$ podman ps

# 如果需要进入容器查看执行情况,参考如下命令
# podman exec -it tomcat-10 bash

# 每次都启动新容器方式创建servcie //--new参数,每次启动都删除旧容器,启动一个新容器 
$ podman generate systemd --restart-policy=always -n --new -f tomcat-10

查看启动文件:

$ cat container-tomcat-10.service

内容如下:

# container-tomcat-10.service
# autogenerated by Podman 3.4.4
# Sun Mar 10 12:31:31 CST 2024

[Unit]
Description=Podman container-tomcat-10.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
ExecStartPre=/bin/rm -f %t/%n.ctr-id
ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon --replace -d --name tomcat-10 -p 8080:8080 -p 8009:8009 -v /home/podman/.dockers/tomcat/webapps:/usr/local/tomcat/webapps -v /home/podman/.dockers/tomcat/logs:/usr/local/tomcat/logs localhost/tomcat-10
ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all

[Install]
WantedBy=default.target

需要额外注意的一个地方是,给出的路径必须是完整路径 “/home/podman/.dockers/tomcat/webapps”,不能是 “~/.dockers/tomcat/webapps”,Systemd不能正确展开 “~” ,导致路径找不到,从而在启动的时候失败,报告错误代码 125

Systemd 配置,开机/系统重启自动启动服务:

# 保存到 ~/.config/systemd/user/
$ mkdir .config

$ mkdir .config/systemd

$ mkdir .config/systemd/user

# 保存到/etc/systemd/system/
$ mv container-tomcat-10.service ~/.config/systemd/user/

# 以当前用户身份刷新配置文件,让其生效
$ systemctl --user daemon-reload

# 设置容器开机自启,并且现在启动
$ systemctl --user enable --now ~/.config/systemd/user/container-tomcat-10.service

# 如果需要进入容器查看执行情况,参考如下命令
# podman exec -it tomcat-10 bash

# 测试,重启系统,观察是否能开机自动启动
$ sudo reboot

后续 WAR 包存储到 ~/.dockers/tomcat/webapps 目录下即可进行正常访问。

参考链接


把系统从Struts2 迁移到 Spring MVC六大步总结

基于struts的系统迁移到SpringMVC架构上来,共分六部曲,让系统一部一部迁移过来,本文讲的知识点以Struts2 to Spring4,但是针对其他应用场景也是可以参考的。

Step 1: 替换基本的框架库。

Firstly while migrating from struts to spring we have to replace our struts related libraries with spring libraries in lib folder.

I have mentioned basic libraries of both struts and spring for your clarification.

Struts basic libraries :
  1. struts.jar
  2. struts-legacy.jar
  3. etc.. 

Have you ever use :   Javadoc comment in Java

Spring basic libraries :

  1. standard.jar
  2. org.springframework.asm-4.0.1.RELEASE-A.jar
  3. org.springframework.beans-4.0.1.RELEASE-A.jar
  4. org.springframework.context-4.0.1.RELEASE-A.jar
  5. org.springframework.core-4.0.1.RELEASE-A.jar
  6. org.springframework.expression-4.0.1.RELEASE-A.jar
  7. org.springframework.web.servlet-4.0.1.RELEASE-A.jar
  8. org.springframework.web-4.0.1.RELEASE-A.jar
  9. etc..

Step 2: 修改web.xml配置文件

In this step we have to remove Action filter dispatcher for the web.xml and add Spring dipatcher servlet as Front controller
Work on new technology  :  Create and manage cloud applications using Java

In Strut application web.xml look like as follows
<?xml version="1.0" encoding="UTF-8"?>  
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">  
  <display-name>Struts2MyFirstApp</display-name>  
  <filter>  
        <filter-name>struts2</filter-name>  
        <filter-class>  
            org.apache.struts2.dispatcher.FilterDispatcher  
        </filter-class>  
    </filter>  

<filter-mapping>  
        <filter-name>struts2</filter-name>  
        <url-pattern>/*</url-pattern>  
    </filter-mapping>  
    <welcome-file-list>  
        <welcome-file>Login.jsp</welcome-file>  
    </welcome-file-list>  
</web-app>
In Spring application web.xml look like as follows
<?xml version="1.0" encoding="UTF-8"?>  
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">  
  <display-name>dispatcher</display-name>  
  <servlet>  
    <servlet-name>dispatcher</servlet-name>  
    <servlet-class>  
            org.springframework.web.servlet.DispatcherServlet  
        </servlet-class>  
    <load-on-startup>1</load-on-startup>  
  </servlet>  
  <servlet-mapping>  
    <servlet-name>dispatcher</servlet-name>  
    <url-pattern>/</url-pattern>  
  </servlet-mapping>  
</web-app>

Step 3: 替换Struts本身的配置文件

Now replace all struts configuration files to spring configuration file as follows

In Struts applivation struts configuration file-
<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE struts PUBLIC  
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"  
    "http://struts.apache.org/dtds/struts-2.0.dtd">  
   
<struts>  
    <constant name="struts.enable.DynamicMethodInvocation" value="false" />  
    <constant name="struts.devMode" value="false" />  
    <constant name="struts.custom.i18n.resources" value="myapp" />  
   
    <package name="default" extends="struts-default" namespace="/">  
        <action name="login" class="com.geekonjavaonjava.struts2.login.LoginAction">  
            <result name="success">Welcome.jsp</result>  
            <result name="error">Login.jsp</result>  
        </action>  
    </package>  
</struts>  
In Spring application spring configuration file as follows
<?xml version="1.0" encoding="UTF-8"?>    
<beans xmlns="http://www.springframework.org/schema/beans"    
     xmlns:context="http://www.springframework.org/schema/context"    
     xmlns:p="http://www.springframework.org/schema/p"      
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    
     xsi:schemaLocation="http://www.springframework.org/schema/beans    
    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd    
    http://www.springframework.org/schema/context    
    http://www.springframework.org/schema/context/spring-context-4.0.xsd">    
        
     <context:component-scan base-package="com.geekonjavaonjava.spring.login.controller" />    
        
     <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">    
        <property name="prefix">    
          <value>/WEB-INF/views/</value>    
        </property>    
        <property name="suffix">    
          <value>.jsp</value>    
        </property>    
      </bean>    
</beans>

Here, <context:component-scan> tag is used, so that spring will load all the components from given package i.e. " com.geekonjavaonjava.spring.login.controller".

Use this in Struts2 : Get value of struts property tag into jsp variable

We can use different view resolver, here I have used InternalResourceViewResolver. In which prefix and suffix are used to resolve the view by prefixing and suffixing values to the object returned by ModelAndView in action class.

Step 4: 修改JSP文件

While migration an application from struts to spring we need to change in jsp file as following

Firstly replace all tlds-
<%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean" %>  
<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %>  
<%@ taglib uri="http://struts.apache.org/tags-logic" prefix="logic" %>  
<%@ taglib uri="http://struts.apache.org/tags-tiles" prefix="tiles" %>
Replace these with following spring taglib's :
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>  
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
In Struts :
<html:form action="/addLogin" method="post">
In Spring :
<form:form method="POST" commandName="loginForm" name="loginForm" action="login.do"> 

Here commandName is going to map with corresponding formbean for that jsp. Next we will see, how action is getting called with spring 4 annotations.

Step 5: 修改Action 类文件

Now following changes need to be done in action classes for struts to spring migration using annotations-

Struts Action:
package com.geekonjavaonjava.struts2.login;  
  
import com.opensymphony.xwork2.ActionSupport;  
  
/** 
 * @author geekonjava 
 * 
 */  
@SuppressWarnings("serial")  
public class LoginAction  extends ActionSupport{  
 private String username;  
    private String password;  
      
 public String execute() {  
     
        if (this.username.equals("geekonjava")   
                && this.password.equals("sweety")) {  
            return "success";  
        } else {  
         addActionError(getText("error.login"));  
            return "error";  
        }  
    }  
  
 public String getUsername() {  
  return username;  
 }  
  
 public void setUsername(String username) {  
  this.username = username;  
 }  
  
 public String getPassword() {  
  return password;  
 }  
  
 public void setPassword(String password) {  
  this.password = password;  
 }  
   
}
Spring action
package com.geekonjavaonjava.spring.login.controller;  
  
import org.springframework.stereotype.Controller;  
import org.springframework.ui.ModelMap;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RequestMethod;  
  
/** 
 * @author GeekOnJava 
 * 
 */  
@Controller  
public class LoginController {  
   
 @RequestMapping(value="/login.do", method = RequestMethod.GET)  
 public String doLogin(ModelMap model, LoginForm loginForn) {  
 if (this.username.equals("geekonjava")   
                && this.password.equals("sweety")) {  
            model.addAttribute("message", "Login Success");  
        } else {  
            model.addAttribute("message", "Login Failure");  
        }  
         return "home";  
   
 }  
}

Step 6: 修改前端验证机制

In struts JSP file validation changes as follows
<% ActionErrors actionErrors = (ActionErrors)request.getAttribute("org.apache.struts.action.ERROR"); %>
In Spring JSP file as follows-
<form:errors path="*" cssClass="error" /> 

参考链接