JoyLau's Blog

JoyLau 的技术学习与思考

背景

docker 容器启动, 通过 docker logs -f container 可以实时查看日志

但是控制台输出的日志太多,会怎么样,容器里控制台输出的日志在宿主机什么位置?

有时容器输出太多,运行时间长了后,会把磁盘撑满…

解释

docker 里容器的日志都属于标准输出(stdout)
每个 container 都是一个特殊的进程,由 docker daemon 创建并启动,docker daemon 来守护和管理

docker daemon 有一个默认的日志驱动程序,默认为json-file
json-file 会把所有容器的标准输出和标准错误以json格式写入文件中,这个文件每行记录一个标准输出或标准错误并用时间戳注释

修改配置

  1. vim /etc/docker/daemon.json

  2. 增加一条:{“log-driver”: “none”} (也可以添加{“log-opts”: {“max-size”: “10m” }} 来控制log文件的大小)

  3. 重新加载配置文件并重启docker服务: systemctl daemon-reload

docker-compose 配置

1
2
3
4
    logging: 
# driver: "json-file"
options:
max-size: "1g"

这样就不需要修改 daemon.json 配置文件了

查看日志位置

  1. docker inspect container_id | grep log
  2. 进入上述目录
  3. du -sh *

解决

在 idea 以前的版本里,在 Preferences | Build, Execution, Deployment | Gradle 去掉勾选 Offline work 即可

但是在最新版 2019.2 里,需要点击 gradle 面板里最上面一排小扳手左边一个图标,取消离线模式

  1. fork 模式下
  • 使用命令参数 pm2 start app.js --node-args="--harmony"
  • json 文件添加配置: "node_args" : "--harmony"
  1. cluster 模式下
    使用上一篇的方法 require("babel-register");
    在更改配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"apps": [
{
"name": "my_name",
"cwd": "./",
"script": "bin/start",
"instances" : "max",
"exec_mode" : "cluster",
"log_date_format": "YYYY-MM-DD HH:mm Z",
"error_file": "./logs/error.log",
"watch": ["routes"]
}
]
}

这里需要注意:

  1. exec_mode 要改为 cluster, instances 为实例数, max 为 CPU 的核心数,
  2. script 里配置的直接就是 js 文件,不需要加 node 命令(如 “script”: “node bin/start”) ,否则启动会报错,我踩过这个坑

  1. package.json 添加
1
2
3
4
5
6
7
8
9
10
"babel": {
"presets": [
"es2015"
]
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-es2015": "^6.24.1",
"babel-register": "^6.26.0"
}
  1. npm install

  2. 有 2 种方法可配置

  • 第一种: 启动命令改为: ./node_modules/.bin/babel-node app.js
  • 第二种: 在 app.js 头部里添加 require("babel-register");

lombok 依赖编译报错

在gradle4.7以后对于加入依赖lombok方式发生变化,gradle4.7版本以前,可以直接如下引用:

1
compile("org.projectlombok:lombok:1.18.2")或者compileOnly("org.projectlombok:lombok:1.18.2")

在gradle5.0这种方式会产生警告,在gradle5.0里面会直接报编译错误

有 2 中解决方式:

  1. 官方推荐

开发依赖:

1
2
3
4
annotationProcessor 'org.projectlombok:lombok:1.18.2'

compileOnly 'org.projectlombok:lombok:1.18.2'

测试依赖:

1
2
3
testAnnotationProcessor 'org.projectlombok:lombok:1.18.2'

testCompileOnly 'org.projectlombok:lombok:1.18.2'
  1. gradle-lombok插件方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
repositories {                 
mavenCentral()
}


plugins {

id 'net.ltgt.apt' version '0.10'

}

dependencies {

compileOnly 'org.projectlombok:lombok:1.18.2'

apt "org.projectlombok:lombok:1.18.2"
}

log4j 报错

错误信息:

1
2
3
Errors occurred while build effective model from /Users/joylau/.gradle/caches/modules-2/files-2.1/log4j/log4j/1.2.16/88efb1b8d3d993fe339e9e2b201c75eed57d4c65/log4j-1.2.16.pom:
'build.plugins.plugin[io.spring.gradle.dependencymanagement.org.apache.maven.plugins:maven-antrun-plugin].dependencies.dependency.scope' for junit:junit:jar must be one of [compile, runtime, system] but is 'test'. in log4j:log4j:1.2.16

这是因为 Log4J 1.2.16 的 pom 中存在一个Bug。1.2.16 已经在 2010 年停止更新了
可以通过声明对 log4j:log4j:1.2.17 的显式依赖
或通过依赖关系管理确保使用 1.2.17 来解决

1
implementation("log4j:log4j:1.2.17")

容器启动时初始化数据的方法

  1. 编写好脚本,支持 .sql;.sh;.sql.gz
  2. 容器启动时, 将脚本挂载到容器的 /docker-entrypoint-initdb.d 目录下即可

可就是这么简单的操作,我却没有成功…

注意

该方法只在初始化数据库的时候起作用,意思是,当你想把 mariadb 的数据目录 /var/lib/mysql 挂载到本地盘上,那么 该目下有文件时,放置的脚本将不会执行

Electron 自动更新的方法

  1. 使用 Electron 自己提供的 autoUpdater 模块
  2. 使用更新服务器
  3. 自己实现自动更新逻辑

为什么说经过了一系列的折腾呢, 因为前 2 中方式都没有解决我的问题,最后我是自己实现了自动更新的逻辑
没有解决我的问题是因为我需要兼顾到 mac 平台和 Windows 平台,然而 mac 平台比较麻烦,代码需要签名
我自己亲测方式一和方式二在 mac 平台上都需要代码签名, 而签名代码需要注册苹果开发者账号,需要付年费
于是这 2 条路就走不通了

最后我决定自己实现更新的逻辑

更新逻辑分析

  1. 自动触发或手动触发软件更新检查
  2. 服务器版本号大于本地版本才出现更新提示
  3. 对于更新,无非就是卸载之前的版本,安装新下载的安装包
  4. 软件的打包我选择的是 Electron Builder, 分别打成 dmg , setup.exe , app.zip
  5. 更新的时候先从服务器下载新版本
  6. 下载完成后对于安装包的安装分平台来说

Windows 下的更新

  1. Windows 下的安装包是 exe 可执行文件,安装包本身是有处理逻辑在里面的
  2. 于是我们只需要将安装包下载到临时目录,然后再软件里打开它,再退出软件,剩下的安装步骤交给用户
  3. 有一点需要注意的是,NSIS 的新安装包在安装前会自动卸载掉之前的版本,不过不会提示用户,我们可以在 nsis 脚本里加一个提示

MacOS 下的更新

  1. 相比于 Windows 下的安装包, macOS 下的 dmg 安装包就没有什么逻辑了,直接打开,然后将 app 文件拖到 Applications 目录中即可完成安装
  2. 于是有 2 中方法可选
  3. 一. 挂载 dmg, 找到挂载目录,在 mac 下是 /Volumes 目录下; 删除 /Applications 下的 app, 将 /Volumes 下的 app 拷贝到 /Applications 目录下; 再卸载 dmg; 重启应用即可,该方法可实现类似无缝更新的效果
  4. 二. 和方法一一个道理,只不过不是挂载 dmg 来查找 app, 直接解压 app.zip 压缩文件即可得到 app ,在使用相同的方式覆盖即可.

软件的版本控制

可以采取一个 json 文件来记录个版本的更新记录, 这里给个参考:

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
[
{
"version": "1.1.0",
"force": false,
"time": "2019-09-14",
"download": {
"winSetup": "",
"dmg": "",
"appZip": ""
},
"description": [
"1. 修复若干 BUG,稳定性提升"
]
},
{
"version": "1.0.0",
"force": false,
"time": "2019-09-01",
"download": {
"winSetup": "",
"dmg": "",
"appZip": ""
},
"description": [
"1. 全新界面,主体功能完成"
]
}
]

代码参考

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import $ from 'jquery';
import semver from 'semver';
import request from 'request';
import progress from 'request-progress';

//global.fs = require('fs');
//global.cp = require('child_process');
const fs = window.fs;
const cp = window.cp;
const electron = window.electron;
const {app, shell} = electron.remote;

state = {
check: true,
latest: {},
// wait,download,install,error
update: 'wait',
downloadState: {}
};

// 检查更新
$.ajax({
url: appConfig.updateCheckURL,
timeout: 10000,
type: 'GET',
cache:false,
success: function (data) {
let latest = data[0];
if(semver.satisfies(latest.version, '>' + app.getVersion())){
if (latest.force) {
that.updateVersion();
}
}
},
complete: function (XMLHttpRequest, status) {
that.setState({
check: false
})
}
});


updateVersion(){
let that = this;
const platform = osInfo.platform();
try {
const downloadUrl = platform === 'darwin' ? this.state.latest.download.dmg : platform === 'win32' ? this.state.latest.download.winSetup : '';
if (downloadUrl === '') return;

const downloadUrlArr = downloadUrl.split("/");

const filename = downloadUrlArr[downloadUrlArr.length-1];

const savePath = osInfo.tmpdir() + '/' + filename;

const _request = request(downloadUrl);
progress(_request, {
// throttle: 2000, // Throttle the progress event to 2000ms, defaults to 1000ms
// delay: 1000, // Only start to emit after 1000ms delay, defaults to 0ms
// lengthHeader: 'x-transfer-length' // Length header to use, defaults to content-length
})
.on('progress', function (state) {
// The state is an object that looks like this:
// {
// percent: 0.5, // Overall percent (between 0 to 1)
// speed: 554732, // The download speed in bytes/sec
// size: {
// total: 90044871, // The total payload size in bytes
// transferred: 27610959 // The transferred payload size in bytes
// },
// time: {
// elapsed: 36.235, // The total elapsed seconds since the start (3 decimals)
// remaining: 81.403 // The remaining seconds to finish (3 decimals)
// }
// }
that.setState({downloadState: state})
})
.on('error', function (err) {
that.setState({
downloadState:{
error: true
}
})
})
.on('end', function () {
if (that.state.update === 'error') return;
that.setState({
update: 'install',
});

setTimeout(function () {
if (platform === 'darwin'){
const appName = pjson.build.productName;
const appVersion = app.getVersion();
console.info(appName,appVersion);
// 挂载
cp.execSync(`hdiutil attach '${savePath}' -nobrowse`, {
stdio: ['ignore', 'ignore', 'ignore']
});

// 覆盖原 app
cp.execSync(`rm -rf '/Applications/${appName}.app' && cp -R '/Volumes/${appName} ${appVersion}/${appName}.app' '/Applications/${appName}.app'`);

// 卸载挂载的 dmg
cp.execSync(`hdiutil eject '/Volumes/${appName} ${appVersion}'`, {
stdio: ['ignore', 'ignore', 'ignore']
});

// 重启
app.relaunch();
app.quit();
}

if (platform === 'win32') {
shell.openItem(savePath);
setTimeout(function () {
app.quit();
},1500)
}
},2000)
})
.pipe(fs.createWriteStream(savePath));

that.setState({update:'download'});
} catch (e) {
console.info(e);
that.setState({
update: 'error',
});
}
}

打包的资源无法包含 build 目录

1
2
3
4
5
6
"files": [
"**/*",
"build/",
"!build/static/js/*.js.map",
"!src/"
],

同时该配置也可防止源码被打包进去,

查看打包后的目录结构

"asar": false,

引入外部文件

1
2
3
4
5
6
"extraResources": [
{
"from": "./LICENSE",
"to": "./../LICENSE.txt"
}
],

定义安装包输出目录

1
2
3
"directories": {
"output": "dist"
},

Windows 环境下打出 32 位和 64 位二合一包

1
2
3
4
5
6
7
8
9
10
11
"win": {
"target": [
{
"target": "nsis",
"arch": [
"ia32",
"x64"
]
}
]
},

打出的 mac 包写入数据到 Info.plist 文件

1
2
3
4
5
6
7
8
9
10
11
12
"mac": {
"extendInfo": {
"URL types": [
{
"URL identifier": "Joy Security",
"URL Schemes": [
"joy-security"
]
}
]
}
},

NSIS 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"nsis": {
"oneClick": false, // 一键安装
"perMachine": true, // 为所有用户安装
"allowElevation": true, // 允许权限提升, 设置 false 的话需要重新允许安装程序
"allowToChangeInstallationDirectory": true, // 允许更改安装目录
"installerIcon": "./public/icons/win.ico",
"uninstallerIcon": "./public/icons/win_uninstall.ico",
"installerHeaderIcon": "./public/icons/win.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "Joy Security",
"license": "./LICENSE",
"include": "./public/nsis/installer.nsh" // 包含的脚本
}

NSIS 脚本

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
!macro customHeader

!macroend

!macro preInit

!macroend

!macro customInit
# guid=7e51495b-3f4d-5235-aadd-5636863064f0
ReadRegStr $0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{7e51495b-3f4d-5235-aadd-5636863064f0}" "UninstallString"
${If} $0 != ""
MessageBox MB_ICONINFORMATION|MB_TOPMOST "检测到系统中已安装本程序,将卸载旧版本" IDOK
# ExecWait $0 $1
${EndIf}
!macroend

!macro customInstall

!macroend

!macro customInstallMode
# set $isForceMachineInstall or $isForceCurrentInstall
# to enforce one or the other modes.
#set $isForceMachineInstall
!macroend

NSIS 引入 license 文件包含中文的问题

当引入的 license 文件里有中文时, 在 Windows (中文操作系统) 平台下打包需要 GBK 编码, 在 macOS 下,GBK 编码会直接报错,需要修改为 UTF-8 编码

0%