diff --git a/.gitignore b/.gitignore
index b701baf..5176f85 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 /releases
 /built
 /tools
+/logs
 /.idea
 /Config.json
 dist/
diff --git a/API.ZH.md b/API.ZH.md
index 515ef81..75a9144 100644
--- a/API.ZH.md
+++ b/API.ZH.md
@@ -133,13 +133,38 @@ Authorization: Basic WFpCOjEyNDg=
 
 ---
 
+### 执行命令:`/device/exec`
+
+参数:`cmd`、`args`以及`device`(设备ID)
+
+示例:
+```http request
+POST http://localhost:8000/api/device/exec HTTP/1.1
+Host: localhost:8000
+Content-Length: 116
+Content-Type: application/x-www-form-urlencoded
+User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.47
+Origin: http://localhost:8000
+Referer: http://localhost:8000/
+
+cmd=taskkill&args=%2Ff%20%2Fim%20regedit.exe&device=bc7e49f8f794f80ffb0032a4ba516c86d76041bf2023e1be6c5dda3b1ee0cf4c
+```
+
+```
+{
+    "code": 0
+}
+```
+
+---
+
 ### 获取截屏:`/device/screenshot/get`
 
 参数:`device`(设备ID)
 
 如果截屏获取成功,则会直接以图片的形式输出。
 <br />
-如果截屏失败,如下响应会被输出(错误信息不一定是这一个)。
+如果截屏失败,如下响应会被输出(错误信息不唯一)。
 
 ```
 {
@@ -199,7 +224,7 @@ Authorization: Basic WFpCOjEyNDg=
 <br />
 如果存在同名文件,则会被**覆盖**!
 
-Example:
+示例:
 ```http request
 POST http://localhost:8000/api/device/file/upload?path=D%3A%5C&file=Test.txt&device=bc7e49f8f794f80ffb0032a4ba516c86d76041bf2023e1be6c5dda3b1ee0cf4c HTTP/1.1
 Host: localhost:8000
@@ -212,7 +237,7 @@ Referer: http://localhost:8000/
 Hello World.
 ```
 
-如果文件上传成功,则`code`为`1`。
+如果文件上传成功,则`code`为`0`。
 <br />
 文件`D:\Test.txt`会写入:`Hello World.`。
 
diff --git a/API.md b/API.md
index 94fc688..b74a8e3 100644
--- a/API.md
+++ b/API.md
@@ -20,7 +20,7 @@ Example:
 Authorization: Basic WFpCOjEyNDg=
 ```
 
-After basic authentication, server will assign you a `Authorization` cookie.
+After basic authentication, server will assign you an `Authorization` cookie.
 <br />
 You can use this token cookie to authenticate rest of your requests.
 
@@ -130,6 +130,31 @@ For example, when you call `/device/restart`, your device will restart.
 
 ---
 
+### Execute command: `/device/exec`
+
+Parameters: `cmd`, `args` and `device` (device ID)
+
+Example:
+```http request
+POST http://localhost:8000/api/device/exec HTTP/1.1
+Host: localhost:8000
+Content-Length: 116
+Content-Type: application/x-www-form-urlencoded
+User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.47
+Origin: http://localhost:8000
+Referer: http://localhost:8000/
+
+cmd=taskkill&args=%2Ff%20%2Fim%20regedit.exe&device=bc7e49f8f794f80ffb0032a4ba516c86d76041bf2023e1be6c5dda3b1ee0cf4c
+```
+
+```
+{
+    "code": 0
+}
+```
+
+---
+
 ### Take screenshot: `/device/screenshot/get`
 
 Parameters: `device` (device ID)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 56368fe..8037c9f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,15 @@
+## v0.1.6
+
+* Optimize: potential crash problem of explorer.
+* Add: set config through command line arguments.
+* Add: log system.
+
+* 优化:文件管理器可能导致服务崩溃。
+* 添加:支持通过命令行设置相关配置。
+* 新增:日志机制。
+
+
+
 ## v0.1.5
 
 * Optimize: performance of desktop viewer on Windows.
diff --git a/README.ZH.md b/README.ZH.md
index 79197b8..440fc4f 100644
--- a/README.ZH.md
+++ b/README.ZH.md
@@ -24,9 +24,11 @@
 
 **本项目及其源代码和发行版,旨在用于学习和交流。**
 <br />
+**禁止用于任何非法用途!**
+<br />
 **使用本项目所带来的风险由使用者本人承担。**
 <br />
-**作者和开发者不会对你的错误使用而造成的损害承担任何责任。**
+**作者和开发者不会对你错误使用而造成的损害承担任何责任。**
 
 **数据无价,在点击任何按钮、输入任何命令之前,请三思。**
 
@@ -40,14 +42,19 @@
 
 * 从 [Releases](https://github.com/XZB-1248/Spark/releases) 页面下载对应系统的可执行文件。
 * 解压文件,**不要删除**`built`文件夹。
-* 在目录下创建一个名为`Config.json`的配置文件,修改其中的信息。
+* 在目录下创建一个名为`config.json`的配置文件,设置好相关的配置信息。
 
   ```json
   {
       "listen": ":8000",
-      "salt": "some random string",
+      "salt": "随机字符串(英文数字符号,小于24位)",
       "auth": {
-          "username": "password"
+          "用户名": "密码(英文数字符号)"
+      },
+      "log": {
+          "level": "info",
+          "path": "./logs",
+          "days": 7
       }
   }
   ```
diff --git a/README.md b/README.md
index a5a4c7a..85cce6a 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,8 @@ server forever.
 
 **THIS PROJECT, ITS SOURCE CODE, AND ITS RELEASES SHOULD ONLY BE USED FOR EDUCATIONAL PURPOSES.**
 <br />
+**ALL ILLEGAL USAGE IS PROHIBITED!**
+<br />
 **YOU SHALL USE THIS PROJECT AT YOUR OWN RISK.**
 <br />
 **THE AUTHORS AND DEVELOPERS ARE NOT RESPONSIBLE FOR ANY DAMAGE CAUSED BY YOUR MISUSE OF THIS PROJECT.**
@@ -41,14 +43,19 @@ Only local installation are available yet.
 ### Local installation
 * Get prebuilt executable file from [Releases](https://github.com/XZB-1248/Spark/releases) page.
 * Extract all files and **do not** delete `built` directory.
-* Create a configuration file named `Config.json` and set your own salt.
+* Create a configuration file named `config.json` and set your own salt.
 
   ```json
   {
       "listen": ":8000",
-      "salt": "some random string",
+      "salt": "some random string length <= 24",
       "auth": {
           "username": "password"
+      },
+      "log": {
+          "level": "info",
+          "path": "./logs",
+          "days": 7
       }
   }
   ```
diff --git a/client/client.go b/client/client.go
index cbe14c8..1d1e33c 100644
--- a/client/client.go
+++ b/client/client.go
@@ -8,8 +8,6 @@ import (
 	"crypto/aes"
 	"crypto/cipher"
 	"math/big"
-	"net/http"
-	_ "net/http/pprof"
 	"os"
 	"os/exec"
 	"strings"
@@ -21,18 +19,18 @@ import (
 func init() {
 	golog.SetTimeFormat(`2006/01/02 15:04:05`)
 
-	if len(strings.Trim(config.CfgBuffer, "\x19")) == 0 {
+	if len(strings.Trim(config.ConfigBuffer, "\x19")) == 0 {
 		os.Exit(0)
 		return
 	}
 
 	// Convert first 2 bytes to int, which is the length of the encrypted config.
-	dataLen := int(big.NewInt(0).SetBytes([]byte(config.CfgBuffer[:2])).Uint64())
-	if dataLen > len(config.CfgBuffer)-2 {
+	dataLen := int(big.NewInt(0).SetBytes([]byte(config.ConfigBuffer[:2])).Uint64())
+	if dataLen > len(config.ConfigBuffer)-2 {
 		os.Exit(0)
 		return
 	}
-	cfgBytes := []byte(config.CfgBuffer[2 : 2+dataLen])
+	cfgBytes := utils.StringToBytes(config.ConfigBuffer, 2, 2+dataLen)
 	cfgBytes, err := decrypt(cfgBytes[16:], cfgBytes[:16])
 	if err != nil {
 		os.Exit(0)
@@ -49,7 +47,6 @@ func init() {
 }
 
 func main() {
-	go http.ListenAndServe(`:6060`, nil)
 	update()
 	core.Start()
 }
diff --git a/client/common/common.go b/client/common/common.go
index 5568c8c..c88cbe9 100644
--- a/client/common/common.go
+++ b/client/common/common.go
@@ -18,12 +18,12 @@ type Conn struct {
 	secretHex string
 }
 
+const MaxMessageSize = (2 << 15) + 1024
+
 var WSConn *Conn
 var Mutex = &sync.Mutex{}
 var HTTP = CreateClient()
 
-const MaxMessageSize = 32768 + 1024
-
 func CreateConn(wsConn *ws.Conn, secret []byte) *Conn {
 	return &Conn{
 		Conn:      wsConn,
@@ -42,7 +42,7 @@ func (wsConn *Conn) SendData(data []byte) error {
 	if WSConn == nil {
 		return errors.New(`${i18n|wsClosed}`)
 	}
-	wsConn.SetWriteDeadline(Now.Add(5 * time.Second))
+	wsConn.SetWriteDeadline(utils.Now.Add(5 * time.Second))
 	defer wsConn.SetWriteDeadline(time.Time{})
 	return wsConn.WriteMessage(ws.BinaryMessage, data)
 }
@@ -68,7 +68,7 @@ func (wsConn *Conn) SendPack(pack any) error {
 	if WSConn == nil {
 		return errors.New(`${i18n|wsClosed}`)
 	}
-	wsConn.SetWriteDeadline(Now.Add(5 * time.Second))
+	wsConn.SetWriteDeadline(utils.Now.Add(5 * time.Second))
 	defer wsConn.SetWriteDeadline(time.Time{})
 	return wsConn.WriteMessage(ws.BinaryMessage, data)
 }
diff --git a/client/config/config.go b/client/config/config.go
index 573fcd3..5f3fc42 100644
--- a/client/config/config.go
+++ b/client/config/config.go
@@ -16,10 +16,10 @@ type Cfg struct {
 
 // Localhost for my development only.
 // Shall be commented out when development is done.
-//var CfgBuffer = "\x00\xcd\xc6\x68\x5d\xf5\x83\x53\x1c\x49\xa2\x35\x7b\x5b\xaf\xf2\x9e\x6d\x74\x00\x95\x23\x73\x00\x77\xa0\xe1\x46\x64\xd2\x33\x2b\x04\xb2\xca\x70\xda\x4b\xed\xec\x43\x6b\xeb\x6e\x10\x53\x6e\x62\x13\x3c\xb1\x0a\xdd\xc0\x48\x2d\x77\xfa\x4a\x9b\x26\xb5\x1b\x50\x62\x05\xcc\xc9\x3b\x22\xf5\x19\x5b\xac\x41\x74\xc9\x9e\x02\x9f\xe8\x75\xce\x3a\xe0\x50\x67\x0f\x81\x01\xca\x47\x0d\xb2\x09\x8b\x74\x6c\xfd\xc5\x73\xf9\x2a\xf0\x13\x52\xb7\x79\xff\xeb\xab\xcd\x9f\xe8\xb7\xae\xff\xa9\x50\xb2\x90\x11\x35\x4d\x94\x6e\x67\x55\x37\x66\x58\x21\xc0\x0d\xab\x3b\x6f\xc4\x00\x56\xd6\x06\xa0\x7e\x73\xdf\x46\x76\xe0\xb3\x89\x0d\xa2\x33\x07\x39\x81\x2b\x59\x30\x24\xc7\x4f\xe9\xb9\xf6\x3c\xb6\x24\xc5\x44\xde\xe6\x66\x66\x92\x49\xe1\x38\x50\xff\xb5\xf3\x20\xb9\x15\x60\x4a\xdf\xba\xd5\xae\x85\x7e\x3f\x8a\xf0\xb8\xf5\x23\x39\xf0\x46\x11\x64\x42\x04\x8c\xf0\x8a\x5e\xc7\x43\xd2\x0c\x89\xd1\xc4\x14\x26\xb1\x67\x64\x28\x77\xf4\xc8\xf3\x51\x69\xba\xf2\xca\xfa\x2f\x11\xe0\x8d\x6c\x4e\x8c\xb7\x28\xf5\x2a\x67\xe3\x8f\xf0\x7f\x79\xc5\xa5\x1a\xb5\xa1\x22\xe9\x55\x61\xdd\xce\x39\x13\x4b\xdd\x19\xf1\x5c\x86\x9b\x16\x89\x45\xba\x16\x68\xfc\x88\x4b\xd5\x13\xa4\x7e\x26\xce\x35\x2d\x42\x4d\x21\xf1\xc3\x6d\xf5\x64\x16\xc9\x05\xed\x9b\x6c\xbf\x26\xe3\xad\x40\x1d\xc6\x64\x03\xb9\xcb\xca\x3c\x62\x5d\x07\x6b\x07\x8b\xa9\x86\x60\x27\x28\xe7\xa3\xc2\x8d\x6f\xc0\x3d\x8e\x14\xa6\xcc\xe0\x50\x51\x22\x20\x6b\x16\x10\xe9\xe0\x4a\xd2\x4e\x77\xc8\xd1\xf7\x60\x4c\xed\xca\x3f\x1e\x13\x0a\x2e\x84\x15\xd3\xf6\x3e\x13\x4e\x68\xaf\xfd\x7a\xd7\x5b\xaa\x5b\x28\x7c\x3f\xb3\xd0\xd0"
+//var ConfigBuffer = "\x00\xcd\xc6\x68\x5d\xf5\x83\x53\x1c\x49\xa2\x35\x7b\x5b\xaf\xf2\x9e\x6d\x74\x00\x95\x23\x73\x00\x77\xa0\xe1\x46\x64\xd2\x33\x2b\x04\xb2\xca\x70\xda\x4b\xed\xec\x43\x6b\xeb\x6e\x10\x53\x6e\x62\x13\x3c\xb1\x0a\xdd\xc0\x48\x2d\x77\xfa\x4a\x9b\x26\xb5\x1b\x50\x62\x05\xcc\xc9\x3b\x22\xf5\x19\x5b\xac\x41\x74\xc9\x9e\x02\x9f\xe8\x75\xce\x3a\xe0\x50\x67\x0f\x81\x01\xca\x47\x0d\xb2\x09\x8b\x74\x6c\xfd\xc5\x73\xf9\x2a\xf0\x13\x52\xb7\x79\xff\xeb\xab\xcd\x9f\xe8\xb7\xae\xff\xa9\x50\xb2\x90\x11\x35\x4d\x94\x6e\x67\x55\x37\x66\x58\x21\xc0\x0d\xab\x3b\x6f\xc4\x00\x56\xd6\x06\xa0\x7e\x73\xdf\x46\x76\xe0\xb3\x89\x0d\xa2\x33\x07\x39\x81\x2b\x59\x30\x24\xc7\x4f\xe9\xb9\xf6\x3c\xb6\x24\xc5\x44\xde\xe6\x66\x66\x92\x49\xe1\x38\x50\xff\xb5\xf3\x20\xb9\x15\x60\x4a\xdf\xba\xd5\xae\x85\x7e\x3f\x8a\xf0\xb8\xf5\x23\x39\xf0\x46\x11\x64\x42\x04\x8c\xf0\x8a\x5e\xc7\x43\xd2\x0c\x89\xd1\xc4\x14\x26\xb1\x67\x64\x28\x77\xf4\xc8\xf3\x51\x69\xba\xf2\xca\xfa\x2f\x11\xe0\x8d\x6c\x4e\x8c\xb7\x28\xf5\x2a\x67\xe3\x8f\xf0\x7f\x79\xc5\xa5\x1a\xb5\xa1\x22\xe9\x55\x61\xdd\xce\x39\x13\x4b\xdd\x19\xf1\x5c\x86\x9b\x16\x89\x45\xba\x16\x68\xfc\x88\x4b\xd5\x13\xa4\x7e\x26\xce\x35\x2d\x42\x4d\x21\xf1\xc3\x6d\xf5\x64\x16\xc9\x05\xed\x9b\x6c\xbf\x26\xe3\xad\x40\x1d\xc6\x64\x03\xb9\xcb\xca\x3c\x62\x5d\x07\x6b\x07\x8b\xa9\x86\x60\x27\x28\xe7\xa3\xc2\x8d\x6f\xc0\x3d\x8e\x14\xa6\xcc\xe0\x50\x51\x22\x20\x6b\x16\x10\xe9\xe0\x4a\xd2\x4e\x77\xc8\xd1\xf7\x60\x4c\xed\xca\x3f\x1e\x13\x0a\x2e\x84\x15\xd3\xf6\x3e\x13\x4e\x68\xaf\xfd\x7a\xd7\x5b\xaa\x5b\x28\x7c\x3f\xb3\xd0\xd0"
 
 // None
-var CfgBuffer = "\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19"
+var ConfigBuffer = "\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19"
 
 // COMMIT means this commit hash, help to identify version and self upgrade.
 var COMMIT = ``
diff --git a/client/core/core.go b/client/core/core.go
index 6900a3b..f984b11 100644
--- a/client/core/core.go
+++ b/client/core/core.go
@@ -91,7 +91,7 @@ func reportWS(wsConn *common.Conn) error {
 	if err != nil {
 		return err
 	}
-	common.WSConn.SetReadDeadline(common.Now.Add(5 * time.Second))
+	common.WSConn.SetReadDeadline(utils.Now.Add(5 * time.Second))
 	_, data, err := common.WSConn.ReadMessage()
 	common.WSConn.SetReadDeadline(time.Time{})
 	if err != nil {
@@ -116,7 +116,7 @@ func checkUpdate(wsConn *common.Conn) error {
 		return nil
 	}
 	resp, err := common.HTTP.R().
-		SetBody(config.CfgBuffer).
+		SetBody(config.ConfigBuffer).
 		SetQueryParam(`os`, runtime.GOOS).
 		SetQueryParam(`arch`, runtime.GOARCH).
 		SetQueryParam(`commit`, config.COMMIT).
diff --git a/client/core/handler.go b/client/core/handler.go
index 928a377..3299ad1 100644
--- a/client/core/handler.go
+++ b/client/core/handler.go
@@ -11,7 +11,9 @@ import (
 	"Spark/modules"
 	"github.com/kataras/golog"
 	"os"
+	"os/exec"
 	"reflect"
+	"strings"
 )
 
 var handlers = map[string]func(pack modules.Packet, wsConn *common.Conn){
@@ -40,6 +42,7 @@ var handlers = map[string]func(pack modules.Packet, wsConn *common.Conn){
 	`pingDesktop`:    pingDesktop,
 	`killDesktop`:    killDesktop,
 	`getDesktop`:     getDesktop,
+	`execCommand`:    execCommand,
 }
 
 func ping(pack modules.Packet, wsConn *common.Conn) {
@@ -131,6 +134,8 @@ func initTerminal(pack modules.Packet, wsConn *common.Conn) {
 	err := terminal.InitTerminal(pack)
 	if err != nil {
 		wsConn.SendCallback(modules.Packet{Act: `initTerminal`, Code: 1, Msg: err.Error()}, pack)
+	} else {
+		wsConn.SendCallback(modules.Packet{Act: `initTerminal`, Code: 0}, pack)
 	}
 }
 
@@ -318,6 +323,8 @@ func initDesktop(pack modules.Packet, wsConn *common.Conn) {
 	err := desktop.InitDesktop(pack)
 	if err != nil {
 		wsConn.SendCallback(modules.Packet{Act: `initDesktop`, Code: 1, Msg: err.Error()}, pack)
+	} else {
+		wsConn.SendCallback(modules.Packet{Act: `initDesktop`, Code: 0}, pack)
 	}
 }
 
@@ -332,3 +339,34 @@ func killDesktop(pack modules.Packet, wsConn *common.Conn) {
 func getDesktop(pack modules.Packet, wsConn *common.Conn) {
 	desktop.GetDesktop(pack)
 }
+
+func execCommand(pack modules.Packet, wsConn *common.Conn) {
+	var proc *exec.Cmd
+	var cmd, args string
+	if val, ok := pack.Data[`cmd`]; !ok {
+		wsConn.SendCallback(modules.Packet{Code: 1, Msg: `${i18n|invalidParameter}`}, pack)
+		return
+	} else {
+		cmd = val.(string)
+	}
+	if val, ok := pack.Data[`args`]; !ok {
+		wsConn.SendCallback(modules.Packet{Code: 1, Msg: `${i18n|invalidParameter}`}, pack)
+		return
+	} else {
+		args = val.(string)
+	}
+	if len(args) == 0 {
+		proc = exec.Command(cmd)
+	} else {
+		proc = exec.Command(cmd, strings.Split(args, ` `)...)
+	}
+	err := proc.Start()
+	if err != nil {
+		wsConn.SendCallback(modules.Packet{Code: 1, Msg: err.Error()}, pack)
+	} else {
+		wsConn.SendCallback(modules.Packet{Code: 0, Data: map[string]any{
+			`pid`: proc.Process.Pid,
+		}}, pack)
+		proc.Process.Release()
+	}
+}
diff --git a/client/service/desktop/desktop.go b/client/service/desktop/desktop.go
index a7e4616..795ed64 100644
--- a/client/service/desktop/desktop.go
+++ b/client/service/desktop/desktop.go
@@ -300,7 +300,7 @@ func InitDesktop(pack modules.Packet) error {
 	desktop := &session{
 		event:    pack.Event,
 		rawEvent: rawEvent,
-		lastPack: common.Unix,
+		lastPack: utils.Unix,
 		escape:   false,
 		channel:  make(chan message, 4),
 		lock:     &sync.Mutex{},
@@ -345,7 +345,7 @@ func PingDesktop(pack modules.Packet) {
 		return
 	} else {
 		desktop = val.(*session)
-		desktop.lastPack = common.Unix
+		desktop.lastPack = utils.Unix
 	}
 }
 
diff --git a/client/service/file/file.go b/client/service/file/file.go
index f804306..abb63f5 100644
--- a/client/service/file/file.go
+++ b/client/service/file/file.go
@@ -115,17 +115,21 @@ func FetchFile(dir, file, bridge string) error {
 }
 
 func getTempFile(dir, file string) (string, os.FileMode) {
-	exists := true
-	tempFile := ``
-	for i := 0; exists; i++ {
-		tempFile = path.Join(dir, file+`.tmp.`+strconv.Itoa(i))
-		stat, err := os.Stat(tempFile)
-		if os.IsNotExist(err) {
-			exists = false
+	fileMode := os.FileMode(0644)
+	origin := path.Join(dir, file)
+	stat, err := os.Stat(origin)
+	if stat != nil {
+		fileMode = stat.Mode()
+		tempFile := ``
+		for i := 0; i < 5; i++ {
+			tempFile = path.Join(dir, file+`.tmp.`+strconv.Itoa(i))
+			stat, err = os.Stat(tempFile)
+			if os.IsNotExist(err) {
+				return tempFile, fileMode
+			}
 		}
-		return tempFile, stat.Mode()
 	}
-	return tempFile, 0644
+	return origin, fileMode
 }
 
 func RemoveFiles(files []string) error {
diff --git a/client/service/terminal/terminal_others.go b/client/service/terminal/terminal_others.go
index a865fc7..467c3ff 100644
--- a/client/service/terminal/terminal_others.go
+++ b/client/service/terminal/terminal_others.go
@@ -5,6 +5,7 @@ package terminal
 import (
 	"Spark/client/common"
 	"Spark/modules"
+	"Spark/utils"
 	"encoding/hex"
 	"errors"
 	"github.com/creack/pty"
@@ -33,7 +34,7 @@ func InitTerminal(pack modules.Packet) error {
 	termSession := &terminal{
 		pty:      ptySession,
 		event:    pack.Event,
-		lastPack: common.Unix,
+		lastPack: utils.Unix,
 	}
 	terminals.Set(pack.Data[`terminal`].(string), termSession)
 	go func() {
@@ -44,7 +45,7 @@ func InitTerminal(pack modules.Packet) error {
 			common.WSConn.SendCallback(modules.Packet{Act: `outputTerminal`, Data: map[string]any{
 				`output`: hex.EncodeToString(buffer),
 			}}, pack)
-			termSession.lastPack = common.Unix
+			termSession.lastPack = utils.Unix
 			if err != nil {
 				common.WSConn.SendCallback(modules.Packet{Act: `quitTerminal`}, pack)
 				break
@@ -77,7 +78,7 @@ func InputTerminal(pack modules.Packet) error {
 	}
 	terminal := val.(*terminal)
 	terminal.pty.Write(data)
-	terminal.lastPack = common.Unix
+	terminal.lastPack = utils.Unix
 	return nil
 }
 
@@ -140,7 +141,7 @@ func PingTerminal(pack modules.Packet) {
 		return
 	} else {
 		termSession = val.(*terminal)
-		termSession.lastPack = common.Unix
+		termSession.lastPack = utils.Unix
 	}
 }
 
diff --git a/client/service/terminal/terminal_windows.go b/client/service/terminal/terminal_windows.go
index d6f4b79..d493092 100644
--- a/client/service/terminal/terminal_windows.go
+++ b/client/service/terminal/terminal_windows.go
@@ -3,6 +3,7 @@ package terminal
 import (
 	"Spark/client/common"
 	"Spark/modules"
+	"Spark/utils"
 	"encoding/hex"
 	"io"
 	"os/exec"
@@ -14,6 +15,7 @@ import (
 type terminal struct {
 	lastPack int64
 	event    string
+	stop     bool
 	cmd      *exec.Cmd
 	stdout   *io.ReadCloser
 	stderr   *io.ReadCloser
@@ -26,7 +28,11 @@ func init() {
 	defer func() {
 		recover()
 	}()
-	syscall.NewLazyDLL(`kernel32.dll`).NewProc(`SetConsoleCP`).Call(65001)
+	{
+		kernel32 := syscall.NewLazyDLL(`kernel32.dll`)
+		kernel32.NewProc(`SetConsoleCP`).Call(65001)
+		kernel32.NewProc(`SetConsoleOutputCP`).Call(65001)
+	}
 	go healthCheck()
 }
 
@@ -34,34 +40,28 @@ func InitTerminal(pack modules.Packet) error {
 	cmd := exec.Command(getTerminal())
 	stdout, err := cmd.StdoutPipe()
 	if err != nil {
-		cmd.Process.Kill()
-		cmd.Process.Release()
 		return err
 	}
 	stderr, err := cmd.StderrPipe()
 	if err != nil {
-		cmd.Process.Kill()
-		cmd.Process.Release()
 		return err
 	}
 	stdin, err := cmd.StdinPipe()
 	if err != nil {
-		cmd.Process.Kill()
-		cmd.Process.Release()
 		return err
 	}
 	termSession := &terminal{
 		cmd:      cmd,
+		stop:     false,
 		event:    pack.Event,
 		stdout:   &stdout,
 		stderr:   &stderr,
 		stdin:    &stdin,
-		lastPack: common.Unix,
+		lastPack: utils.Unix,
 	}
-	terminals.Set(pack.Data[`terminal`].(string), termSession)
 
 	readSender := func(rc io.ReadCloser) {
-		for {
+		for !termSession.stop {
 			buffer := make([]byte, 512)
 			n, err := rc.Read(buffer)
 			buffer = buffer[:n]
@@ -69,8 +69,9 @@ func InitTerminal(pack modules.Packet) error {
 			common.WSConn.SendCallback(modules.Packet{Act: `outputTerminal`, Data: map[string]any{
 				`output`: hex.EncodeToString(buffer),
 			}}, pack)
-			termSession.lastPack = common.Unix
+			termSession.lastPack = utils.Unix
 			if err != nil {
+				termSession.stop = true
 				common.WSConn.SendCallback(modules.Packet{Act: `quitTerminal`}, pack)
 				break
 			}
@@ -79,7 +80,12 @@ func InitTerminal(pack modules.Packet) error {
 	go readSender(stdout)
 	go readSender(stderr)
 
-	cmd.Start()
+	err = cmd.Start()
+	if err != nil {
+		termSession.stop = true
+		return err
+	}
+	terminals.Set(pack.Data[`terminal`].(string), termSession)
 	return nil
 }
 
@@ -105,7 +111,7 @@ func InputTerminal(pack modules.Packet) error {
 	}
 	terminal := val.(*terminal)
 	(*terminal.stdin).Write(data)
-	terminal.lastPack = common.Unix
+	terminal.lastPack = utils.Unix
 	return nil
 }
 
@@ -142,7 +148,7 @@ func PingTerminal(pack modules.Packet) {
 		return
 	} else {
 		termSession = val.(*terminal)
-		termSession.lastPack = common.Unix
+		termSession.lastPack = utils.Unix
 	}
 }
 
diff --git a/modules/modules.go b/modules/modules.go
index c1664f6..4532fa9 100644
--- a/modules/modules.go
+++ b/modules/modules.go
@@ -3,19 +3,19 @@ package modules
 import "reflect"
 
 type Packet struct {
-	Code  int                    `json:"code"`
-	Act   string                 `json:"act,omitempty"`
-	Msg   string                 `json:"msg,omitempty"`
-	Data  map[string]interface{} `json:"data,omitempty"`
-	Event string                 `json:"event,omitempty"`
+	Code  int            `json:"code"`
+	Act   string         `json:"act,omitempty"`
+	Msg   string         `json:"msg,omitempty"`
+	Data  map[string]any `json:"data,omitempty"`
+	Event string         `json:"event,omitempty"`
 }
 
 type CommonPack struct {
-	Code  int         `json:"code"`
-	Act   string      `json:"act,omitempty"`
-	Msg   string      `json:"msg,omitempty"`
-	Data  interface{} `json:"data,omitempty"`
-	Event string      `json:"event,omitempty"`
+	Code  int    `json:"code"`
+	Act   string `json:"act,omitempty"`
+	Msg   string `json:"msg,omitempty"`
+	Data  any    `json:"data,omitempty"`
+	Event string `json:"event,omitempty"`
 }
 
 type Device struct {
@@ -55,7 +55,7 @@ type Net struct {
 	Recv uint64 `json:"recv"`
 }
 
-func (p *Packet) GetData(key string, t reflect.Kind) (interface{}, bool) {
+func (p *Packet) GetData(key string, t reflect.Kind) (any, bool) {
 	if p.Data == nil {
 		return nil, false
 	}
diff --git a/server/common/common.go b/server/common/common.go
index 0f8268f..245f1a0 100644
--- a/server/common/common.go
+++ b/server/common/common.go
@@ -14,6 +14,8 @@ import (
 	"strings"
 )
 
+const MaxMessageSize = (2 << 15) + 1024
+
 var Melody = melody.New()
 var Devices = cmap.New()
 
@@ -67,6 +69,27 @@ func Decrypt(data []byte, session *melody.Session) ([]byte, bool) {
 	return dec, true
 }
 
+func GetAddrIP(addr net.Addr) string {
+	switch addr.(type) {
+	case *net.TCPAddr:
+		return addr.(*net.TCPAddr).IP.String()
+	case *net.UDPAddr:
+		return addr.(*net.UDPAddr).IP.String()
+	case *net.IPAddr:
+		return addr.(*net.IPAddr).IP.String()
+	default:
+		return addr.String()
+	}
+}
+
+func GetRealIP(ctx *gin.Context) string {
+	addr, ok := ctx.Request.Context().Value(`ClientIP`).(string)
+	if !ok {
+		return GetRemoteAddr(ctx)
+	}
+	return addr
+}
+
 func GetRemoteAddr(ctx *gin.Context) string {
 	if remote, ok := ctx.RemoteIP(); ok {
 		if remote.IsLoopback() {
diff --git a/server/common/log.go b/server/common/log.go
new file mode 100644
index 0000000..30884de
--- /dev/null
+++ b/server/common/log.go
@@ -0,0 +1,123 @@
+package common
+
+import (
+	"Spark/modules"
+	"Spark/server/config"
+	"Spark/utils"
+	"Spark/utils/melody"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"github.com/kataras/golog"
+	"io"
+	"os"
+	"time"
+)
+
+var logWriter *os.File
+var disposed bool
+
+func init() {
+	setLogDst := func() {
+		var err error
+		if logWriter != nil {
+			logWriter.Close()
+		}
+		if config.Config.Log.Level == `disable` || disposed {
+			golog.SetOutput(os.Stdout)
+			return
+		}
+		os.Mkdir(config.Config.Log.Path, 0666)
+		now := utils.Now.Add(time.Second)
+		logFile := fmt.Sprintf(`%s/%s.log`, config.Config.Log.Path, now.Format(`2006-01-02`))
+		logWriter, err = os.OpenFile(logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
+		if err != nil {
+			golog.Warn(`Failed to open log file: %v`, err)
+		}
+		golog.SetOutput(io.MultiWriter(os.Stdout, logWriter))
+
+		staleDate := time.Unix(now.Unix()-int64(config.Config.Log.Days*86400)-86400, 0)
+		staleLog := fmt.Sprintf(`%s/%s.log`, config.Config.Log.Path, staleDate.Format(`2006-01-02`))
+		os.Remove(staleLog)
+	}
+	setLogDst()
+	go func() {
+		waitSecs := 86400 - (utils.Now.Hour()*3600 + utils.Now.Minute()*60 + utils.Now.Second())
+		<-time.After(time.Duration(waitSecs) * time.Second)
+		setLogDst()
+		for range time.NewTicker(time.Second * 86400).C {
+			setLogDst()
+		}
+	}()
+}
+
+func getLog(ctx any, event, status, msg string, args map[string]any) string {
+	if args == nil {
+		args = map[string]any{}
+	}
+	args[`event`] = event
+	if len(msg) > 0 {
+		args[`msg`] = msg
+	}
+	if len(status) > 0 {
+		args[`status`] = status
+	}
+	if ctx != nil {
+		var connUUID string
+		var targetInfo bool
+		switch ctx.(type) {
+		case *gin.Context:
+			c := ctx.(*gin.Context)
+			args[`from`] = GetRealIP(c)
+			connUUID, targetInfo = c.Request.Context().Value(`ConnUUID`).(string)
+		case *melody.Session:
+			s := ctx.(*melody.Session)
+			args[`from`] = GetAddrIP(s.GetWSConn().UnderlyingConn().RemoteAddr())
+			if deviceConn, ok := args[`deviceConn`]; ok {
+				delete(args, `deviceConn`)
+				connUUID = deviceConn.(*melody.Session).UUID
+				targetInfo = true
+			}
+		}
+		if targetInfo {
+			val, ok := Devices.Get(connUUID)
+			if ok {
+				device := val.(*modules.Device)
+				args[`target`] = map[string]any{
+					`name`: device.Hostname,
+					`ip`:   device.WAN,
+				}
+			}
+		}
+	}
+	output, _ := utils.JSON.MarshalToString(args)
+	return output
+}
+
+func Info(ctx any, event, status, msg string, args map[string]any) {
+	golog.Infof(getLog(ctx, event, status, msg, args))
+}
+
+func Warn(ctx any, event, status, msg string, args map[string]any) {
+	golog.Warnf(getLog(ctx, event, status, msg, args))
+}
+
+func Error(ctx any, event, status, msg string, args map[string]any) {
+	golog.Error(getLog(ctx, event, status, msg, args))
+}
+
+func Fatal(ctx any, event, status, msg string, args map[string]any) {
+	golog.Fatalf(getLog(ctx, event, status, msg, args))
+}
+
+func Debug(ctx any, event, status, msg string, args map[string]any) {
+	golog.Debugf(getLog(ctx, event, status, msg, args))
+}
+
+func CloseLog() {
+	disposed = true
+	golog.SetOutput(os.Stdout)
+	if logWriter != nil {
+		logWriter.Close()
+		logWriter = nil
+	}
+}
diff --git a/server/common/time.go b/server/common/time.go
deleted file mode 100644
index 4615592..0000000
--- a/server/common/time.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package common
-
-import "time"
-
-var Now time.Time = time.Now()
-var Unix int64 = Now.Unix()
-
-// To prevent call time.Now().Unix() too often.
-func init() {
-	go func() {
-		for now := range time.NewTicker(time.Second).C {
-			Now = now
-			Unix = now.Unix()
-		}
-	}()
-}
diff --git a/server/config/config.go b/server/config/config.go
index afadaa5..407f5f0 100644
--- a/server/config/config.go
+++ b/server/config/config.go
@@ -1,31 +1,112 @@
 package config
 
 import (
+	"Spark/utils"
+	"bytes"
+	"flag"
+	"github.com/kataras/golog"
 	"os"
-	"path/filepath"
 )
 
-type Cfg struct {
-	Debug struct {
-		Pprof bool `json:"pprof"`
-		Gin   bool `json:"gin"`
-	} `json:"debug,omitempty"`
-	Listen  string            `json:"listen"`
-	Salt    string            `json:"salt"`
-	Auth    map[string]string `json:"auth"`
-	StdSalt []byte            `json:"-"`
+type config struct {
+	Listen    string            `json:"listen"`
+	Salt      string            `json:"salt"`
+	Auth      map[string]string `json:"auth"`
+	Log       *log              `json:"log"`
+	SaltBytes []byte            `json:"-"`
+}
+type log struct {
+	Level string `json:"level"`
+	Path  string `json:"path"`
+	Days  uint   `json:"days"`
 }
-
-var Config Cfg
-var BuiltPath = getBuiltPath()
 
 // COMMIT is hash of this commit, for auto upgrade.
 var COMMIT = ``
+var Config config
+var BuiltPath = `./built/%v_%v`
+
+func init() {
+	golog.SetTimeFormat(`2006/01/02 15:04:05`)
 
-func getBuiltPath() string {
-	dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
-	if err != nil {
-		return `./built/%v_%v`
+	var (
+		err                      error
+		configData               []byte
+		configPath, listen, salt string
+		username, password       string
+		logLevel, logPath        string
+		logDays                  uint
+	)
+	flag.StringVar(&configPath, `config`, `config.json`, `config file path, default: config.json`)
+	flag.StringVar(&listen, `listen`, `:8000`, `required, listen address, default: :8000`)
+	flag.StringVar(&salt, `salt`, ``, `required, salt of server`)
+	flag.StringVar(&username, `username`, ``, `username of web interface`)
+	flag.StringVar(&password, `password`, ``, `password of web interface`)
+	flag.StringVar(&logLevel, `log-level`, `info`, `log level, default: info`)
+	flag.StringVar(&logPath, `log-path`, `./logs`, `log file path, default: ./logs`)
+	flag.UintVar(&logDays, `log-days`, 7, `max days of logs, default: 7`)
+	flag.Parse()
+
+	if len(configPath) > 0 {
+		configData, err = os.ReadFile(configPath)
+		if err != nil {
+			configData, err = os.ReadFile(`Config.json`)
+			if err != nil {
+				fatal(map[string]any{
+					`event`:  `CONFIG_LOAD`,
+					`status`: `fail`,
+					`msg`:    err.Error(),
+				})
+				return
+			}
+		}
+		err = utils.JSON.Unmarshal(configData, &Config)
+		if err != nil {
+			fatal(map[string]any{
+				`event`:  `CONFIG_PARSE`,
+				`status`: `fail`,
+				`msg`:    err.Error(),
+			})
+			return
+		}
+		if Config.Log == nil {
+			Config.Log = &log{
+				Level: `info`,
+				Path:  `./logs`,
+				Days:  7,
+			}
+		}
+	} else {
+		Config = config{
+			Listen: listen,
+			Salt:   salt,
+			Auth: map[string]string{
+				username: password,
+			},
+			Log: &log{
+				Level: logLevel,
+				Path:  logPath,
+				Days:  logDays,
+			},
+		}
 	}
-	return filepath.Join(dir, `built/%v_%v`)
+
+	if len(Config.Salt) > 24 {
+		fatal(map[string]any{
+			`event`:  `CONFIG_PARSE`,
+			`status`: `fail`,
+			`msg`:    `length of salt should less than 24`,
+		})
+		return
+	}
+	Config.SaltBytes = []byte(Config.Salt)
+	Config.SaltBytes = append(Config.SaltBytes, bytes.Repeat([]byte{25}, 24)...)
+	Config.SaltBytes = Config.SaltBytes[:24]
+
+	golog.SetLevel(utils.If(len(Config.Log.Level) == 0, `info`, Config.Log.Level))
+}
+
+func fatal(args map[string]any) {
+	output, _ := utils.JSON.MarshalToString(args)
+	golog.Fatal(output)
 }
diff --git a/server/handler/bridge/bridge.go b/server/handler/bridge/bridge.go
index 04a735a..a0f0e1a 100644
--- a/server/handler/bridge/bridge.go
+++ b/server/handler/bridge/bridge.go
@@ -2,7 +2,7 @@ package bridge
 
 import (
 	"Spark/modules"
-	"Spark/server/common"
+	"Spark/utils"
 	"Spark/utils/cmap"
 	"github.com/gin-gonic/gin"
 	"github.com/kataras/golog"
@@ -98,7 +98,7 @@ func BridgePush(ctx *gin.Context) {
 			for {
 				eof := false
 				buf := make([]byte, 2<<14)
-				SrcConn.SetReadDeadline(common.Now.Add(5 * time.Second))
+				SrcConn.SetReadDeadline(utils.Now.Add(5 * time.Second))
 				n, err := bridge.Src.Request.Body.Read(buf)
 				if n == 0 {
 					break
@@ -109,7 +109,7 @@ func BridgePush(ctx *gin.Context) {
 						break
 					}
 				}
-				DstConn.SetWriteDeadline(common.Now.Add(10 * time.Second))
+				DstConn.SetWriteDeadline(utils.Now.Add(10 * time.Second))
 				_, err = bridge.Dst.Writer.Write(buf[:n])
 				if eof || err != nil {
 					break
@@ -152,7 +152,7 @@ func BridgePull(ctx *gin.Context) {
 			for {
 				eof := false
 				buf := make([]byte, 2<<14)
-				SrcConn.SetReadDeadline(common.Now.Add(5 * time.Second))
+				SrcConn.SetReadDeadline(utils.Now.Add(5 * time.Second))
 				n, err := bridge.Src.Request.Body.Read(buf)
 				if n == 0 {
 					break
@@ -163,7 +163,7 @@ func BridgePull(ctx *gin.Context) {
 						break
 					}
 				}
-				DstConn.SetWriteDeadline(common.Now.Add(10 * time.Second))
+				DstConn.SetWriteDeadline(utils.Now.Add(10 * time.Second))
 				_, err = bridge.Dst.Writer.Write(buf[:n])
 				if eof || err != nil {
 					break
@@ -183,7 +183,7 @@ func BridgePull(ctx *gin.Context) {
 
 func AddBridge(ext any, uuid string) *Bridge {
 	bridge := &Bridge{
-		creation: common.Unix,
+		creation: utils.Unix,
 		uuid:     uuid,
 		using:    false,
 		lock:     &sync.Mutex{},
@@ -195,7 +195,7 @@ func AddBridge(ext any, uuid string) *Bridge {
 
 func AddBridgeWithSrc(ext any, uuid string, Src *gin.Context) *Bridge {
 	bridge := &Bridge{
-		creation: common.Unix,
+		creation: utils.Unix,
 		uuid:     uuid,
 		using:    false,
 		lock:     &sync.Mutex{},
@@ -208,7 +208,7 @@ func AddBridgeWithSrc(ext any, uuid string, Src *gin.Context) *Bridge {
 
 func AddBridgeWithDst(ext any, uuid string, Dst *gin.Context) *Bridge {
 	bridge := &Bridge{
-		creation: common.Unix,
+		creation: utils.Unix,
 		uuid:     uuid,
 		using:    false,
 		lock:     &sync.Mutex{},
diff --git a/server/handler/desktop/desktop.go b/server/handler/desktop/desktop.go
index 7d5c8ea..eadf8f1 100644
--- a/server/handler/desktop/desktop.go
+++ b/server/handler/desktop/desktop.go
@@ -13,16 +13,15 @@ import (
 
 type desktop struct {
 	uuid       string
-	event      string
 	device     string
-	targetConn *melody.Session
+	srcConn    *melody.Session
 	deviceConn *melody.Session
 }
 
 var desktopSessions = melody.New()
 
 func init() {
-	desktopSessions.Config.MaxMessageSize = 32768 + 1024
+	desktopSessions.Config.MaxMessageSize = common.MaxMessageSize
 	desktopSessions.HandleConnect(onDesktopConnect)
 	desktopSessions.HandleMessage(onDesktopMessage)
 	desktopSessions.HandleMessageBinary(onDesktopMessage)
@@ -59,7 +58,7 @@ func InitDesktop(ctx *gin.Context) {
 	desktopSessions.HandleRequestWithKeys(ctx.Writer, ctx.Request, nil, gin.H{
 		`Secret`:   secret,
 		`Device`:   device,
-		`LastPack`: common.Unix,
+		`LastPack`: utils.Unix,
 	})
 }
 
@@ -72,7 +71,7 @@ func desktopEventWrapper(desktop *desktop) common.EventCallback {
 				return
 			}
 			if data, ok := pack.Data[`data`]; ok {
-				desktop.targetConn.WriteBinary(*data.(*[]byte))
+				desktop.srcConn.WriteBinary(*data.(*[]byte))
 			}
 			return
 		}
@@ -84,9 +83,16 @@ func desktopEventWrapper(desktop *desktop) common.EventCallback {
 				} else {
 					msg += `${i18n|unknownError}`
 				}
-				sendPack(modules.Packet{Act: `quit`, Msg: msg}, desktop.targetConn)
-				common.RemoveEvent(desktop.event)
-				desktop.targetConn.Close()
+				sendPack(modules.Packet{Act: `quit`, Msg: msg}, desktop.srcConn)
+				common.RemoveEvent(desktop.uuid)
+				desktop.srcConn.Close()
+				common.Warn(desktop.srcConn, `DESKTOP_INIT`, `fail`, msg, map[string]any{
+					`deviceConn`: desktop.deviceConn,
+				})
+			} else {
+				common.Info(desktop.srcConn, `DESKTOP_INIT`, `success`, ``, map[string]any{
+					`deviceConn`: desktop.deviceConn,
+				})
 			}
 			return
 		}
@@ -95,9 +101,12 @@ func desktopEventWrapper(desktop *desktop) common.EventCallback {
 			if len(pack.Msg) > 0 {
 				msg = pack.Msg
 			}
-			sendPack(modules.Packet{Act: `quit`, Msg: msg}, desktop.targetConn)
-			common.RemoveEvent(desktop.event)
-			desktop.targetConn.Close()
+			sendPack(modules.Packet{Act: `quit`, Msg: msg}, desktop.srcConn)
+			common.RemoveEvent(desktop.uuid)
+			desktop.srcConn.Close()
+			common.Info(desktop.srcConn, `DESKTOP_QUIT`, `success`, ``, map[string]any{
+				`deviceConn`: desktop.deviceConn,
+			})
 			return
 		}
 	}
@@ -122,20 +131,21 @@ func onDesktopConnect(session *melody.Session) {
 		session.Close()
 		return
 	}
-	eventUUID := utils.GetStrUUID()
 	desktopUUID := utils.GetStrUUID()
 	desktop := &desktop{
 		uuid:       desktopUUID,
-		event:      eventUUID,
 		device:     device.(string),
-		targetConn: session,
+		srcConn:    session,
 		deviceConn: deviceConn,
 	}
 	session.Set(`Desktop`, desktop)
-	common.AddEvent(desktopEventWrapper(desktop), connUUID, eventUUID)
+	common.AddEvent(desktopEventWrapper(desktop), connUUID, desktopUUID)
 	common.SendPack(modules.Packet{Act: `initDesktop`, Data: gin.H{
 		`desktop`: desktopUUID,
-	}, Event: eventUUID}, deviceConn)
+	}, Event: desktopUUID}, deviceConn)
+	common.Info(desktop.srcConn, `DESKTOP_CONN`, `success`, ``, map[string]any{
+		`deviceConn`: desktop.deviceConn,
+	})
 }
 
 func onDesktopMessage(session *melody.Session, data []byte) {
@@ -146,7 +156,7 @@ func onDesktopMessage(session *melody.Session, data []byte) {
 			desktop := val.(*desktop)
 			common.SendPack(modules.Packet{Act: `killDesktop`, Data: gin.H{
 				`desktop`: desktop.uuid,
-			}, Event: desktop.event}, desktop.deviceConn)
+			}, Event: desktop.uuid}, desktop.deviceConn)
 		}
 		sendPack(modules.Packet{Code: -1}, session)
 		session.Close()
@@ -157,29 +167,33 @@ func onDesktopMessage(session *melody.Session, data []byte) {
 		return
 	}
 	desktop := val.(*desktop)
-	session.Set(`LastPack`, common.Unix)
+	session.Set(`LastPack`, utils.Unix)
 	if pack.Act == `pingDesktop` {
 		common.SendPack(modules.Packet{Act: `pingDesktop`, Data: gin.H{
 			`desktop`: desktop.uuid,
-		}, Event: desktop.event}, desktop.deviceConn)
+		}, Event: desktop.uuid}, desktop.deviceConn)
 		return
 	}
 	if pack.Act == `killDesktop` {
+		common.Info(desktop.srcConn, `DESKTOP_KILL`, `success`, ``, map[string]any{
+			`deviceConn`: desktop.deviceConn,
+		})
 		common.SendPack(modules.Packet{Act: `killDesktop`, Data: gin.H{
 			`desktop`: desktop.uuid,
-		}, Event: desktop.event}, desktop.deviceConn)
+		}, Event: desktop.uuid}, desktop.deviceConn)
 		return
 	}
 	if pack.Act == `getDesktop` {
 		common.SendPack(modules.Packet{Act: `getDesktop`, Data: gin.H{
 			`desktop`: desktop.uuid,
-		}, Event: desktop.event}, desktop.deviceConn)
+		}, Event: desktop.uuid}, desktop.deviceConn)
 		return
 	}
 	session.Close()
 }
 
 func onDesktopDisconnect(session *melody.Session) {
+	common.Info(session, `DESKTOP_CLOSE`, `success`, ``, nil)
 	val, ok := session.Get(`Desktop`)
 	if !ok {
 		return
@@ -190,8 +204,8 @@ func onDesktopDisconnect(session *melody.Session) {
 	}
 	common.SendPack(modules.Packet{Act: `killDesktop`, Data: gin.H{
 		`desktop`: desktop.uuid,
-	}, Event: desktop.event}, desktop.deviceConn)
-	common.RemoveEvent(desktop.event)
+	}, Event: desktop.uuid}, desktop.deviceConn)
+	common.RemoveEvent(desktop.uuid)
 	session.Set(`Desktop`, nil)
 	desktop = nil
 }
@@ -224,7 +238,7 @@ func CloseSessionsByDevice(deviceID string) {
 			return true
 		}
 		if desktop.device == deviceID {
-			sendPack(modules.Packet{Act: `quit`, Msg: `${i18n|desktopSessionClosed}`}, desktop.targetConn)
+			sendPack(modules.Packet{Act: `quit`, Msg: `${i18n|desktopSessionClosed}`}, desktop.srcConn)
 			queue = append(queue, session)
 			return false
 		}
diff --git a/server/handler/file/file.go b/server/handler/file/file.go
index aba0f66..22cf7c5 100644
--- a/server/handler/file/file.go
+++ b/server/handler/file/file.go
@@ -27,16 +27,29 @@ func RemoveDeviceFiles(ctx *gin.Context) {
 	if !ok {
 		return
 	}
+	if len(form.Files) == 0 {
+		ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+		return
+	}
 	trigger := utils.GetStrUUID()
 	common.SendPackByUUID(modules.Packet{Code: 0, Act: `removeFiles`, Data: gin.H{`files`: form.Files}, Event: trigger}, target)
 	ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
 		if p.Code != 0 {
+			common.Warn(ctx, `REMOVE_FILES`, `fail`, p.Msg, map[string]any{
+				`files`: form.Files,
+			})
 			ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
 		} else {
+			common.Info(ctx, `REMOVE_FILES`, `success`, ``, map[string]any{
+				`files`: form.Files,
+			})
 			ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
 		}
 	}, target, trigger, 5*time.Second)
 	if !ok {
+		common.Warn(ctx, `REMOVE_FILES`, `fail`, `timeout`, map[string]any{
+			`files`: form.Files,
+		})
 		ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
 	}
 }
@@ -124,11 +137,14 @@ func GetDeviceFiles(ctx *gin.Context) {
 	wait := make(chan bool)
 	called := false
 	common.AddEvent(func(p modules.Packet, _ *melody.Session) {
-		wait <- false
 		called = true
 		bridge.RemoveBridge(bridgeID)
 		common.RemoveEvent(trigger)
+		common.Warn(ctx, `READ_FILES`, `fail`, p.Msg, map[string]any{
+			`files`: form.Files,
+		})
 		ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+		wait <- false
 	}, target, trigger)
 	instance := bridge.AddBridgeWithDst(nil, bridgeID, ctx)
 	instance.OnPush = func(bridge *bridge.Bridge) {
@@ -177,6 +193,11 @@ func GetDeviceFiles(ctx *gin.Context) {
 		}
 	}
 	instance.OnFinish = func(bridge *bridge.Bridge) {
+		if called {
+			common.Info(ctx, `READ_FILES`, `success`, ``, map[string]any{
+				`files`: form.Files,
+			})
+		}
 		wait <- false
 	}
 	select {
@@ -185,6 +206,9 @@ func GetDeviceFiles(ctx *gin.Context) {
 		if !called {
 			bridge.RemoveBridge(bridgeID)
 			common.RemoveEvent(trigger)
+			common.Warn(ctx, `READ_FILES`, `fail`, `timeout`, map[string]any{
+				`files`: form.Files,
+			})
 			ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
 		} else {
 			<-wait
@@ -203,6 +227,10 @@ func GetDeviceTextFile(ctx *gin.Context) {
 	if !ok {
 		return
 	}
+	if len(form.File) == 0 {
+		ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+		return
+	}
 	bridgeID := utils.GetStrUUID()
 	trigger := utils.GetStrUUID()
 	common.SendPackByUUID(modules.Packet{Code: 0, Act: `uploadTextFile`, Data: gin.H{
@@ -212,11 +240,14 @@ func GetDeviceTextFile(ctx *gin.Context) {
 	wait := make(chan bool)
 	called := false
 	common.AddEvent(func(p modules.Packet, _ *melody.Session) {
-		wait <- false
 		called = true
 		bridge.RemoveBridge(bridgeID)
 		common.RemoveEvent(trigger)
+		common.Warn(ctx, `READ_TEXT_FILE`, `fail`, p.Msg, map[string]any{
+			`file`: form.File,
+		})
 		ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+		wait <- false
 	}, target, trigger)
 	instance := bridge.AddBridgeWithDst(nil, bridgeID, ctx)
 	instance.OnPush = func(bridge *bridge.Bridge) {
@@ -239,6 +270,11 @@ func GetDeviceTextFile(ctx *gin.Context) {
 		ctx.Status(http.StatusOK)
 	}
 	instance.OnFinish = func(bridge *bridge.Bridge) {
+		if called {
+			common.Info(ctx, `READ_TEXT_FILE`, `success`, ``, map[string]any{
+				`file`: form.File,
+			})
+		}
 		wait <- false
 	}
 	select {
@@ -247,6 +283,9 @@ func GetDeviceTextFile(ctx *gin.Context) {
 		if !called {
 			bridge.RemoveBridge(bridgeID)
 			common.RemoveEvent(trigger)
+			common.Warn(ctx, `READ_TEXT_FILE`, `fail`, `timeout`, map[string]any{
+				`file`: form.File,
+			})
 			ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
 		} else {
 			<-wait
@@ -266,18 +305,28 @@ func UploadToDevice(ctx *gin.Context) {
 	if !ok {
 		return
 	}
+	if len(form.File) == 0 || len(form.Path) == 0 {
+		ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+		return
+	}
 	bridgeID := utils.GetStrUUID()
 	trigger := utils.GetStrUUID()
 	wait := make(chan bool)
 	called := false
 	response := false
+	fileDest := path.Join(form.Path, form.File)
+	fileSize := ctx.Request.ContentLength
 	common.AddEvent(func(p modules.Packet, _ *melody.Session) {
-		wait <- false
 		called = true
 		response = true
 		bridge.RemoveBridge(bridgeID)
 		common.RemoveEvent(trigger)
+		common.Warn(ctx, `UPLOAD_FILE`, `fail`, p.Msg, map[string]any{
+			`dest`: fileDest,
+			`size`: fileSize,
+		})
 		ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+		wait <- false
 	}, target, trigger)
 	instance := bridge.AddBridgeWithSrc(nil, bridgeID, ctx)
 	instance.OnPull = func(bridge *bridge.Bridge) {
@@ -293,6 +342,12 @@ func UploadToDevice(ctx *gin.Context) {
 		dst.Header(`Content-Disposition`, fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, form.File, url.PathEscape(form.File)))
 	}
 	instance.OnFinish = func(bridge *bridge.Bridge) {
+		if called {
+			common.Info(ctx, `UPLOAD_FILE`, `success`, ``, map[string]any{
+				`dest`: fileDest,
+				`size`: fileSize,
+			})
+		}
 		wait <- false
 	}
 	common.SendPackByUUID(modules.Packet{Code: 0, Act: `fetchFile`, Data: gin.H{
@@ -310,6 +365,10 @@ func UploadToDevice(ctx *gin.Context) {
 			bridge.RemoveBridge(bridgeID)
 			common.RemoveEvent(trigger)
 			if !response {
+				common.Warn(ctx, `UPLOAD_FILE`, `fail`, `timeout`, map[string]any{
+					`dest`: fileDest,
+					`size`: fileSize,
+				})
 				ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
 			}
 		} else {
diff --git a/server/handler/generate/generate.go b/server/handler/generate/generate.go
index d2c73bf..07ca993 100644
--- a/server/handler/generate/generate.go
+++ b/server/handler/generate/generate.go
@@ -86,7 +86,7 @@ func GenerateClient(ctx *gin.Context) {
 		return
 	}
 	clientUUID := utils.GetUUID()
-	clientKey, err := common.EncAES(clientUUID, config.Config.StdSalt)
+	clientKey, err := common.EncAES(clientUUID, config.Config.SaltBytes)
 	if err != nil {
 		ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`})
 		return
diff --git a/server/handler/handler.go b/server/handler/handler.go
index 59f96c3..9e4efa9 100644
--- a/server/handler/handler.go
+++ b/server/handler/handler.go
@@ -29,6 +29,7 @@ func InitRouter(ctx *gin.RouterGroup) {
 		group.POST(`/device/file/list`, file.ListDeviceFiles)
 		group.POST(`/device/file/text`, file.GetDeviceTextFile)
 		group.POST(`/device/file/get`, file.GetDeviceFiles)
+		group.POST(`/device/exec`, utility.ExecDeviceCmd)
 		group.POST(`/device/list`, utility.GetDevices)
 		group.POST(`/device/:act`, utility.CallDevice)
 		group.POST(`/client/check`, generate.CheckClient)
diff --git a/server/handler/process/process.go b/server/handler/process/process.go
index 248dc9f..12e250d 100644
--- a/server/handler/process/process.go
+++ b/server/handler/process/process.go
@@ -46,11 +46,20 @@ func KillDeviceProcess(ctx *gin.Context) {
 	ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
 		if p.Code != 0 {
 			ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+			common.Warn(ctx, `KILL_PROCESS`, `fail`, p.Msg, map[string]any{
+				`pid`: form.Pid,
+			})
 		} else {
 			ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
+			common.Info(ctx, `KILL_PROCESS`, `success`, ``, map[string]any{
+				`pid`: form.Pid,
+			})
 		}
 	}, target, trigger, 5*time.Second)
 	if !ok {
 		ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
+		common.Warn(ctx, `KILL_PROCESS`, `fail`, `timeout`, map[string]any{
+			`pid`: form.Pid,
+		})
 	}
 }
diff --git a/server/handler/screenshot/screenshot.go b/server/handler/screenshot/screenshot.go
index b02a49a..ab19033 100644
--- a/server/handler/screenshot/screenshot.go
+++ b/server/handler/screenshot/screenshot.go
@@ -24,11 +24,12 @@ func GetScreenshot(ctx *gin.Context) {
 	called := false
 	common.SendPackByUUID(modules.Packet{Code: 0, Act: `screenshot`, Data: gin.H{`bridge`: bridgeID}, Event: trigger}, target)
 	common.AddEvent(func(p modules.Packet, _ *melody.Session) {
-		wait <- false
 		called = true
 		bridge.RemoveBridge(bridgeID)
 		common.RemoveEvent(trigger)
 		ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+		common.Warn(ctx, `TAKE_SCREENSHOT`, `fail`, p.Msg, nil)
+		wait <- false
 	}, target, trigger)
 	instance := bridge.AddBridgeWithDst(nil, bridgeID, ctx)
 	instance.OnPush = func(bridge *bridge.Bridge) {
@@ -37,6 +38,9 @@ func GetScreenshot(ctx *gin.Context) {
 		ctx.Header(`Content-Type`, `image/png`)
 	}
 	instance.OnFinish = func(bridge *bridge.Bridge) {
+		if called {
+			common.Info(ctx, `TAKE_SCREENSHOT`, `success`, ``, nil)
+		}
 		wait <- false
 	}
 	select {
@@ -46,6 +50,7 @@ func GetScreenshot(ctx *gin.Context) {
 			bridge.RemoveBridge(bridgeID)
 			common.RemoveEvent(trigger)
 			ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
+			common.Warn(ctx, `TAKE_SCREENSHOT`, `fail`, `timeout`, nil)
 		} else {
 			<-wait
 		}
diff --git a/server/handler/terminal/terminal.go b/server/handler/terminal/terminal.go
index 8fca215..592a80c 100644
--- a/server/handler/terminal/terminal.go
+++ b/server/handler/terminal/terminal.go
@@ -9,11 +9,11 @@ import (
 	"encoding/hex"
 	"github.com/gin-gonic/gin"
 	"net/http"
+	"reflect"
 )
 
 type terminal struct {
 	uuid       string
-	event      string
 	device     string
 	session    *melody.Session
 	deviceConn *melody.Session
@@ -58,7 +58,7 @@ func InitTerminal(ctx *gin.Context) {
 	terminalSessions.HandleRequestWithKeys(ctx.Writer, ctx.Request, nil, gin.H{
 		`Secret`:   secret,
 		`Device`:   device,
-		`LastPack`: common.Unix,
+		`LastPack`: utils.Unix,
 	})
 }
 
@@ -75,8 +75,15 @@ func terminalEventWrapper(terminal *terminal) common.EventCallback {
 					msg += `${i18n|unknownError}`
 				}
 				sendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session)
-				common.RemoveEvent(terminal.event)
+				common.RemoveEvent(terminal.uuid)
 				terminal.session.Close()
+				common.Warn(terminal.session, `TERMINAL_INIT`, `fail`, msg, map[string]any{
+					`deviceConn`: terminal.deviceConn,
+				})
+			} else {
+				common.Info(terminal.session, `TERMINAL_INIT`, `success`, ``, map[string]any{
+					`deviceConn`: terminal.deviceConn,
+				})
 			}
 			return
 		}
@@ -86,8 +93,11 @@ func terminalEventWrapper(terminal *terminal) common.EventCallback {
 				msg = pack.Msg
 			}
 			sendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session)
-			common.RemoveEvent(terminal.event)
+			common.RemoveEvent(terminal.uuid)
 			terminal.session.Close()
+			common.Info(terminal.session, `TERMINAL_QUIT`, ``, msg, map[string]any{
+				`deviceConn`: terminal.deviceConn,
+			})
 			return
 		}
 		if pack.Act == `outputTerminal` {
@@ -123,19 +133,20 @@ func onTerminalConnect(session *melody.Session) {
 		return
 	}
 	termUUID := utils.GetStrUUID()
-	eventUUID := utils.GetStrUUID()
 	terminal := &terminal{
 		uuid:       termUUID,
-		event:      eventUUID,
 		device:     device.(string),
 		session:    session,
 		deviceConn: deviceConn,
 	}
 	session.Set(`Terminal`, terminal)
-	common.AddEvent(terminalEventWrapper(terminal), connUUID, eventUUID)
+	common.AddEvent(terminalEventWrapper(terminal), connUUID, termUUID)
 	common.SendPack(modules.Packet{Act: `initTerminal`, Data: gin.H{
 		`terminal`: termUUID,
-	}, Event: eventUUID}, deviceConn)
+	}, Event: termUUID}, deviceConn)
+	common.Info(terminal.session, `TERMINAL_CONN`, `success`, ``, map[string]any{
+		`deviceConn`: terminal.deviceConn,
+	})
 }
 
 func onTerminalMessage(session *melody.Session, data []byte) {
@@ -151,16 +162,21 @@ func onTerminalMessage(session *melody.Session, data []byte) {
 		return
 	}
 	terminal := val.(*terminal)
-	session.Set(`LastPack`, common.Unix)
+	session.Set(`LastPack`, utils.Unix)
 	if pack.Act == `inputTerminal` {
 		if pack.Data == nil {
 			return
 		}
-		if input, ok := pack.Data[`input`]; ok {
+		if input, ok := pack.GetData(`input`, reflect.String); ok {
+			rawInput, _ := hex.DecodeString(input.(string))
+			common.Info(terminal.session, `TERMINAL_INPUT`, ``, ``, map[string]any{
+				`deviceConn`: terminal.deviceConn,
+				`input`:      utils.BytesToString(rawInput),
+			})
 			common.SendPack(modules.Packet{Act: `inputTerminal`, Data: gin.H{
 				`input`:    input,
 				`terminal`: terminal.uuid,
-			}, Event: terminal.event}, terminal.deviceConn)
+			}, Event: terminal.uuid}, terminal.deviceConn)
 		}
 		return
 	}
@@ -174,7 +190,7 @@ func onTerminalMessage(session *melody.Session, data []byte) {
 					`width`:    width,
 					`height`:   height,
 					`terminal`: terminal.uuid,
-				}, Event: terminal.event}, terminal.deviceConn)
+				}, Event: terminal.uuid}, terminal.deviceConn)
 			}
 		}
 		return
@@ -183,9 +199,12 @@ func onTerminalMessage(session *melody.Session, data []byte) {
 		if pack.Data == nil {
 			return
 		}
+		common.Info(terminal.session, `TERMINAL_KILL`, `success`, ``, map[string]any{
+			`deviceConn`: terminal.deviceConn,
+		})
 		common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{
 			`terminal`: terminal.uuid,
-		}, Event: terminal.event}, terminal.deviceConn)
+		}, Event: terminal.uuid}, terminal.deviceConn)
 		return
 	}
 	if pack.Act == `ping` {
@@ -194,13 +213,14 @@ func onTerminalMessage(session *melody.Session, data []byte) {
 		}
 		common.SendPack(modules.Packet{Act: `pingTerminal`, Data: gin.H{
 			`terminal`: terminal.uuid,
-		}, Event: terminal.event}, terminal.deviceConn)
+		}, Event: terminal.uuid}, terminal.deviceConn)
 		return
 	}
 	session.Close()
 }
 
 func onTerminalDisconnect(session *melody.Session) {
+	common.Info(session, `TERMINAL_CLOSE`, `success`, ``, nil)
 	val, ok := session.Get(`Terminal`)
 	if !ok {
 		return
@@ -211,8 +231,8 @@ func onTerminalDisconnect(session *melody.Session) {
 	}
 	common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{
 		`terminal`: terminal.uuid,
-	}, Event: terminal.event}, terminal.deviceConn)
-	common.RemoveEvent(terminal.event)
+	}, Event: terminal.uuid}, terminal.deviceConn)
+	common.RemoveEvent(terminal.uuid)
 	session.Set(`Terminal`, nil)
 	terminal = nil
 }
diff --git a/server/handler/utility/utility.go b/server/handler/utility/utility.go
index 7beeca6..95c7c37 100644
--- a/server/handler/utility/utility.go
+++ b/server/handler/utility/utility.go
@@ -7,6 +7,7 @@ import (
 	"Spark/utils"
 	"Spark/utils/melody"
 	"bytes"
+	"context"
 	"crypto/aes"
 	"crypto/cipher"
 	"fmt"
@@ -40,6 +41,7 @@ func CheckForm(ctx *gin.Context, form any) (string, bool) {
 		ctx.AbortWithStatusJSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `${i18n|deviceNotExists}`})
 		return ``, false
 	}
+	ctx.Request = ctx.Request.WithContext(context.WithValue(ctx.Request.Context(), `ConnUUID`, connUUID))
 	return connUUID, true
 }
 
@@ -88,6 +90,12 @@ func OnDevicePack(data []byte, session *melody.Session) error {
 			common.Devices.Remove(exSession)
 		}
 		common.Devices.Set(session.UUID, &pack.Device)
+		common.Info(nil, `CLIENT_ONLINE`, ``, ``, map[string]any{
+			`device`: map[string]any{
+				`name`: pack.Device.Hostname,
+				`ip`:   pack.Device.WAN,
+			},
+		})
 	} else {
 		val, ok := common.Devices.Get(session.UUID)
 		if ok {
@@ -111,36 +119,84 @@ func CheckUpdate(ctx *gin.Context) {
 		Commit string `form:"commit" binding:"required"`
 	}
 	if err := ctx.ShouldBind(&form); err != nil {
-		golog.Error(err)
 		ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
 		return
 	}
 	if form.Commit == config.COMMIT {
 		ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
+		common.Warn(ctx, `CLIENT_UPDATE`, `success`, `latest`, map[string]any{
+			`client`: map[string]any{
+				`os`:     form.OS,
+				`arch`:   form.Arch,
+				`commit`: form.Commit,
+			},
+			`server`: config.COMMIT,
+		})
 		return
 	}
 	tpl, err := os.Open(fmt.Sprintf(config.BuiltPath, form.OS, form.Arch))
 	if err != nil {
 		ctx.AbortWithStatusJSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
+		common.Warn(ctx, `CLIENT_UPDATE`, `fail`, `no prebuild asset`, map[string]any{
+			`client`: map[string]any{
+				`os`:     form.OS,
+				`arch`:   form.Arch,
+				`commit`: form.Commit,
+			},
+			`server`: config.COMMIT,
+		})
 		return
 	}
 
 	const MaxBodySize = 384 // This is size of client config buffer.
 	if ctx.Request.ContentLength > MaxBodySize {
 		ctx.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1})
+		common.Warn(ctx, `CLIENT_UPDATE`, `fail`, `config too large`, map[string]any{
+			`client`: map[string]any{
+				`os`:     form.OS,
+				`arch`:   form.Arch,
+				`commit`: form.Commit,
+			},
+			`server`: config.COMMIT,
+		})
 		return
 	}
 	body, err := ctx.GetRawData()
 	if err != nil {
 		ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: 1})
+		common.Warn(ctx, `CLIENT_UPDATE`, `fail`, `read config fail`, map[string]any{
+			`client`: map[string]any{
+				`os`:     form.OS,
+				`arch`:   form.Arch,
+				`commit`: form.Commit,
+			},
+			`server`: config.COMMIT,
+		})
 		return
 	}
 	session := common.CheckClientReq(ctx)
 	if session == nil {
 		ctx.AbortWithStatusJSON(http.StatusUnauthorized, modules.Packet{Code: 1})
+		common.Warn(ctx, `CLIENT_UPDATE`, `fail`, `check config fail`, map[string]any{
+			`client`: map[string]any{
+				`os`:     form.OS,
+				`arch`:   form.Arch,
+				`commit`: form.Commit,
+			},
+			`server`: config.COMMIT,
+		})
 		return
 	}
 
+	common.Info(ctx, `CLIENT_UPDATE`, `success`, `updating`, map[string]any{
+		`client`: map[string]any{
+			`os`:     form.OS,
+			`arch`:   form.Arch,
+			`commit`: form.Commit,
+		},
+		`server`: config.COMMIT,
+	})
+
 	ctx.Header(`Spark-Commit`, config.COMMIT)
 	ctx.Header(`Accept-Ranges`, `none`)
 	ctx.Header(`Content-Transfer-Encoding`, `binary`)
@@ -171,6 +227,46 @@ func CheckUpdate(ctx *gin.Context) {
 	}
 }
 
+// ExecDeviceCmd execute command on device.
+func ExecDeviceCmd(ctx *gin.Context) {
+	var form struct {
+		Cmd  string `json:"cmd" yaml:"cmd" form:"cmd" binding:"required"`
+		Args string `json:"args" yaml:"args" form:"args"`
+	}
+	target, ok := CheckForm(ctx, &form)
+	if !ok {
+		return
+	}
+	if len(form.Cmd) == 0 {
+		ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+		return
+	}
+	trigger := utils.GetStrUUID()
+	common.SendPackByUUID(modules.Packet{Code: 0, Act: `execCommand`, Data: gin.H{`cmd`: form.Cmd, `args`: form.Args}, Event: trigger}, target)
+	ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
+		if p.Code != 0 {
+			common.Warn(ctx, `EXEC_COMMAND`, `fail`, p.Msg, map[string]any{
+				`cmd`:  form.Cmd,
+				`args`: form.Args,
+			})
+			ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+		} else {
+			common.Info(ctx, `EXEC_COMMAND`, `success`, ``, map[string]any{
+				`cmd`:  form.Cmd,
+				`args`: form.Args,
+			})
+			ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
+		}
+	}, target, trigger, 5*time.Second)
+	if !ok {
+		common.Warn(ctx, `EXEC_COMMAND`, `fail`, `timeout`, map[string]any{
+			`cmd`:  form.Cmd,
+			`args`: form.Args,
+		})
+		ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
+	}
+}
+
 // GetDevices will return all info about all clients.
 func GetDevices(ctx *gin.Context) {
 	devices := map[string]any{}
@@ -199,6 +295,9 @@ func CallDevice(ctx *gin.Context) {
 			}
 		}
 		if !ok {
+			common.Warn(ctx, `CALL_DEVICE`, `fail`, `invalid act`, map[string]any{
+				`act`: act,
+			})
 			ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
 			return
 		}
@@ -211,14 +310,23 @@ func CallDevice(ctx *gin.Context) {
 	common.SendPackByUUID(modules.Packet{Act: act, Event: trigger}, connUUID)
 	ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
 		if p.Code != 0 {
+			common.Warn(ctx, `CALL_DEVICE`, `fail`, p.Msg, map[string]any{
+				`act`: act,
+			})
 			ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
 		} else {
+			common.Info(ctx, `CALL_DEVICE`, `success`, ``, map[string]any{
+				`act`: act,
+			})
 			ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
 		}
 	}, connUUID, trigger, 5*time.Second)
 	if !ok {
 		//This means the client is offline.
 		//So we take this as a success.
+		common.Info(ctx, `CALL_DEVICE`, `success`, ``, map[string]any{
+			`act`: act,
+		})
 		ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
 	}
 }
diff --git a/server/main.go b/server/main.go
index a1aa7ef..3f7c4fc 100644
--- a/server/main.go
+++ b/server/main.go
@@ -28,51 +28,24 @@ import (
 	"Spark/utils/melody"
 	"net/http"
 
-	"github.com/gin-contrib/pprof"
 	"github.com/gin-gonic/gin"
 	"github.com/kataras/golog"
 )
 
+var blocked = cmap.New()
 var lastRequest = time.Now().Unix()
 
 func main() {
-	golog.SetTimeFormat(`2006/01/02 15:04:05`)
-
-	data, err := os.ReadFile(`./Config.json`)
-	if err != nil {
-		golog.Fatal(`Failed to read config file: `, err)
-		return
-	}
-	err = utils.JSON.Unmarshal(data, &config.Config)
-	if err != nil {
-		golog.Fatal(`Failed to parse config file: `, err)
-		return
-	}
-	if len(config.Config.Salt) > 24 {
-		golog.Fatal(`Length of Salt should be less than 24.`)
-		return
-	}
-	config.Config.StdSalt = []byte(config.Config.Salt)
-	config.Config.StdSalt = append(config.Config.StdSalt, bytes.Repeat([]byte{25}, 24)...)
-	config.Config.StdSalt = config.Config.StdSalt[:24]
-
 	webFS, err := fs.NewWithNamespace(`web`)
 	if err != nil {
-		golog.Fatal(`Failed to load static resources: `, err)
+		common.Fatal(nil, `LOAD_STATIC_RES`, `fail`, err.Error(), nil)
 		return
 	}
-	if config.Config.Debug.Gin {
-		gin.SetMode(gin.DebugMode)
-	} else {
-		gin.SetMode(gin.ReleaseMode)
-	}
+	gin.SetMode(gin.ReleaseMode)
 	app := gin.New()
 	app.Use(gin.Recovery())
-	if config.Config.Debug.Pprof {
-		pprof.Register(app)
-	}
 	{
-		handler.AuthHandler = authCheck()
+		handler.AuthHandler = checkAuth()
 		handler.InitRouter(app.Group(`/api`))
 		app.Any(`/ws`, wsHandshake)
 		app.NoRoute(handler.AuthHandler, func(ctx *gin.Context) {
@@ -82,7 +55,7 @@ func main() {
 		})
 	}
 
-	common.Melody.Config.MaxMessageSize = 32768 + 1024
+	common.Melody.Config.MaxMessageSize = common.MaxMessageSize
 	common.Melody.HandleConnect(wsOnConnect)
 	common.Melody.HandleMessage(wsOnMessage)
 	common.Melody.HandleMessageBinary(wsOnMessageBinary)
@@ -93,33 +66,43 @@ func main() {
 		Addr:    config.Config.Listen,
 		Handler: app,
 		ConnContext: func(ctx context.Context, c net.Conn) context.Context {
-			return context.WithValue(ctx, `Conn`, c)
+			ctx = context.WithValue(ctx, `Conn`, c)
+			ctx = context.WithValue(ctx, `ClientIP`, common.GetAddrIP(c.RemoteAddr()))
+			return ctx
 		},
 	}
-	go func() {
-		if err = srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
-			golog.Fatal(`Failed to bind address: `, err)
+	{
+		go func() {
+			err = srv.ListenAndServe()
+		}()
+		if err != nil {
+			common.Fatal(nil, `SERVICE_INIT`, `fail`, err.Error(), nil)
+		} else {
+			common.Info(nil, `SERVICE_INIT`, ``, ``, map[string]any{
+				`listen`: config.Config.Listen,
+			})
 		}
-	}()
+	}
 	quit := make(chan os.Signal, 3)
 	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
 	<-quit
-	golog.Warn(`Server is shutting down ...`)
+	common.Warn(nil, `SERVICE_EXITING`, ``, ``, nil)
 
 	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
 	defer cancel()
 	if err := srv.Shutdown(ctx); err != nil {
-		golog.Fatal(`Server shutdown: `, err)
+		common.Warn(nil, `SERVICE_EXIT`, `error`, err.Error(), nil)
 	}
 	<-ctx.Done()
-	golog.Info(`Server exited.`)
+	common.Warn(nil, `SERVICE_EXIT`, `success`, ``, nil)
+	common.CloseLog()
 }
 
 func wsHandshake(ctx *gin.Context) {
 	if !ctx.IsWebsocket() {
 		// When message is too large to transport via websocket,
 		// client will try to send these data via http.
-		const MaxBodySize = 2 << 18 //524288 512KB
+		const MaxBodySize = 2 << 18 // 524288 512KB
 		if ctx.Request.ContentLength > MaxBodySize {
 			ctx.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1})
 			return
@@ -145,7 +128,7 @@ func wsHandshake(ctx *gin.Context) {
 		ctx.AbortWithStatus(http.StatusUnauthorized)
 		return
 	}
-	decrypted, err := common.DecAES(clientKey, config.Config.StdSalt)
+	decrypted, err := common.DecAES(clientKey, config.Config.SaltBytes)
 	if err != nil || !bytes.Equal(decrypted, clientUUID) {
 		ctx.AbortWithStatus(http.StatusUnauthorized)
 		return
@@ -155,7 +138,7 @@ func wsHandshake(ctx *gin.Context) {
 		`Secret`: []string{hex.EncodeToString(secret)},
 	}, gin.H{
 		`Secret`:   secret,
-		`LastPack`: common.Unix,
+		`LastPack`: utils.Unix,
 		`Address`:  common.GetRemoteAddr(ctx),
 	})
 	if err != nil {
@@ -169,7 +152,7 @@ func wsOnConnect(session *melody.Session) {
 	pingDevice(session)
 }
 
-func wsOnMessage(session *melody.Session, bytes []byte) {
+func wsOnMessage(session *melody.Session, _ []byte) {
 	session.Close()
 }
 
@@ -200,7 +183,7 @@ func wsOnMessageBinary(session *melody.Session, data []byte) {
 		return
 	}
 	if pack.Act == `report` || pack.Act == `setDevice` {
-		session.Set(`LastPack`, common.Unix)
+		session.Set(`LastPack`, utils.Unix)
 		utility.OnDevicePack(data, session)
 		return
 	}
@@ -209,7 +192,7 @@ func wsOnMessageBinary(session *melody.Session, data []byte) {
 		return
 	}
 	common.CallEvent(pack, session)
-	session.Set(`LastPack`, common.Unix)
+	session.Set(`LastPack`, utils.Unix)
 }
 
 func wsOnDisconnect(session *melody.Session) {
@@ -217,6 +200,18 @@ func wsOnDisconnect(session *melody.Session) {
 		deviceInfo := val.(*modules.Device)
 		terminal.CloseSessionsByDevice(deviceInfo.ID)
 		desktop.CloseSessionsByDevice(deviceInfo.ID)
+		common.Info(nil, `CLIENT_OFFLINE`, ``, ``, map[string]any{
+			`device`: map[string]any{
+				`name`: deviceInfo.Hostname,
+				`ip`:   deviceInfo.WAN,
+			},
+		})
+	} else {
+		common.Info(nil, `CLIENT_OFFLINE`, ``, ``, map[string]any{
+			`device`: map[string]any{
+				`ip`: common.GetAddrIP(session.GetWSConn().UnderlyingConn().RemoteAddr()),
+			},
+		})
 	}
 	common.Devices.Remove(session.UUID)
 }
@@ -231,7 +226,7 @@ func wsHealthCheck(container *melody.Melody) {
 		var pingInterval int64 = 3
 		for range time.NewTicker(3 * time.Second).C {
 			tick += 3
-			if tick >= common.Unix-lastRequest {
+			if tick >= utils.Unix-lastRequest {
 				pingInterval = 3
 			}
 			if tick >= 3 && (tick >= pingInterval || tick >= MaxPingInterval) {
@@ -286,7 +281,7 @@ func pingDevice(s *melody.Session) {
 	}, s.UUID, trigger, 3*time.Second)
 }
 
-func authCheck() gin.HandlerFunc {
+func checkAuth() gin.HandlerFunc {
 	// Token as key and update timestamp as value.
 	// Stores authenticated tokens.
 	tokens := cmap.New()
@@ -300,19 +295,30 @@ func authCheck() gin.HandlerFunc {
 				return true
 			})
 			tokens.Remove(queue...)
+			queue = nil
+
+			blocked.IterCb(func(addr string, v any) bool {
+				if now.Unix() > v.(int64) {
+					queue = append(queue, addr)
+				}
+				return true
+			})
+			blocked.Remove(queue...)
 		}
 	}()
 
 	if config.Config.Auth == nil || len(config.Config.Auth) == 0 {
 		return func(ctx *gin.Context) {
-			lastRequest = common.Unix
+			lastRequest = utils.Unix
 			ctx.Next()
 		}
 	}
+
 	auth := gin.BasicAuth(config.Config.Auth)
 	return func(ctx *gin.Context) {
-		now := common.Unix
+		now := utils.Unix
 		passed := false
+
 		if token, err := ctx.Cookie(`Authorization`); err == nil {
 			if tokens.Has(token) {
 				lastRequest = now
@@ -321,11 +327,32 @@ func authCheck() gin.HandlerFunc {
 				return
 			}
 		}
+
 		if !passed {
+			addr := common.GetRealIP(ctx)
+			if expire, ok := blocked.Get(addr); ok {
+				if now < expire.(int64) {
+					ctx.AbortWithStatusJSON(http.StatusTooManyRequests, modules.Packet{Code: 1})
+					return
+				}
+				blocked.Remove(addr)
+			}
+
 			auth(ctx)
+			user, _, _ := ctx.Request.BasicAuth()
+
 			if ctx.IsAborted() {
+				blocked.Set(addr, now+1)
+				user = utils.If(len(user) == 0, `EMPTY`, user)
+				common.Warn(ctx, `LOGIN_ATTEMPT`, `fail`, ``, map[string]any{
+					`user`: user,
+				})
 				return
 			}
+
+			common.Warn(ctx, `LOGIN_ATTEMPT`, `success`, ``, map[string]any{
+				`user`: user,
+			})
 			token := utils.GetStrUUID()
 			tokens.Set(token, now)
 			ctx.Header(`Set-Cookie`, fmt.Sprintf(`Authorization=%s; Path=/; HttpOnly`, token))
@@ -366,7 +393,7 @@ func serveGzip(ctx *gin.Context, statikFS http.FileSystem) bool {
 	}
 	ctx.Header(`Cache-Control`, `max-age=604800`)
 	ctx.Header(`ETag`, etag)
-	ctx.Header(`Expires`, common.Now.Add(7*24*time.Hour).Format(`Mon, 02 Jan 2006 15:04:05 GMT`))
+	ctx.Header(`Expires`, utils.Now.Add(7*24*time.Hour).Format(`Mon, 02 Jan 2006 15:04:05 GMT`))
 
 	ctx.Writer.Header().Del(`Content-Length`)
 	ctx.Header(`Content-Encoding`, `gzip`)
@@ -386,7 +413,7 @@ func serveGzip(ctx *gin.Context, statikFS http.FileSystem) bool {
 				break
 			}
 		}
-		conn.SetWriteDeadline(common.Now.Add(10 * time.Second))
+		conn.SetWriteDeadline(utils.Now.Add(10 * time.Second))
 		_, err = ctx.Writer.Write(buf[:n])
 		if eof || err != nil {
 			break
@@ -408,6 +435,6 @@ func checkCache(ctx *gin.Context, _ http.FileSystem) bool {
 	}
 	ctx.Header(`ETag`, etag)
 	ctx.Header(`Cache-Control`, `max-age=604800`)
-	ctx.Header(`Expires`, common.Now.Add(7*24*time.Hour).Format(`Mon, 02 Jan 2006 15:04:05 GMT`))
+	ctx.Header(`Expires`, utils.Now.Add(7*24*time.Hour).Format(`Mon, 02 Jan 2006 15:04:05 GMT`))
 	return false
 }
diff --git a/client/common/time.go b/utils/time.go
similarity index 94%
rename from client/common/time.go
rename to utils/time.go
index 4615592..b3b0832 100644
--- a/client/common/time.go
+++ b/utils/time.go
@@ -1,4 +1,4 @@
-package common
+package utils
 
 import "time"
 
diff --git a/utils/utils.go b/utils/utils.go
index 3295154..2a8607c 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -8,6 +8,7 @@ import (
 	"crypto/rand"
 	"encoding/hex"
 	"errors"
+	"fmt"
 	jsoniter "github.com/json-iterator/go"
 	"reflect"
 	"unsafe"
@@ -107,6 +108,57 @@ func Decrypt(data []byte, key []byte) ([]byte, error) {
 	return decBuffer, nil
 }
 
+func FormatSize(size int64) string {
+	sizes := []string{`B`, `KB`, `MB`, `GB`, `TB`, `PB`, `EB`, `ZB`, `YB`}
+	i := 0
+	for size >= 1024 && i < len(sizes)-1 {
+		size /= 1024
+		i++
+	}
+	return fmt.Sprintf(`%d%s`, size, sizes[i])
+}
+
+func BytesToString(b []byte, r ...int) string {
+	sh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
+	bytesPtr := sh.Data
+	bytesLen := sh.Len
+	switch len(r) {
+	case 1:
+		r[0] = If(r[0] > bytesLen, bytesLen, r[0])
+		bytesLen -= r[0]
+		bytesPtr += uintptr(r[0])
+	case 2:
+		r[0] = If(r[0] > bytesLen, bytesLen, r[0])
+		bytesLen = If(r[1] > bytesLen, bytesLen, r[1]) - r[0]
+		bytesPtr += uintptr(r[0])
+	}
+	return *(*string)(unsafe.Pointer(&reflect.StringHeader{
+		Data: bytesPtr,
+		Len:  bytesLen,
+	}))
+}
+
+func StringToBytes(s string, r ...int) []byte {
+	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
+	strPtr := sh.Data
+	strLen := sh.Len
+	switch len(r) {
+	case 1:
+		r[0] = If(r[0] > strLen, strLen, r[0])
+		strLen -= r[0]
+		strPtr += uintptr(r[0])
+	case 2:
+		r[0] = If(r[0] > strLen, strLen, r[0])
+		strLen = If(r[1] > strLen, strLen, r[1]) - r[0]
+		strPtr += uintptr(r[0])
+	}
+	return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
+		Data: strPtr,
+		Len:  strLen,
+		Cap:  strLen,
+	}))
+}
+
 func GetSlicePrefix[T any](data *[]T, n int) *[]T {
 	sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(data))
 	return (*[]T)(unsafe.Pointer(&reflect.SliceHeader{
diff --git a/web/public/index.html b/web/public/index.html
index 476109b..9dc5bfd 100644
--- a/web/public/index.html
+++ b/web/public/index.html
@@ -9,7 +9,7 @@
     <script src="./ext-modelist.js"></script>
 </head>
 <body>
-<noscript>You need to enable JavaScript to run this app.</noscript>
-<div id="root"></div>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
 </body>
 </html>
diff --git a/web/src/components/explorer.js b/web/src/components/explorer.js
index 8278d18..21a6cd7 100644
--- a/web/src/components/explorer.js
+++ b/web/src/components/explorer.js
@@ -14,7 +14,7 @@ import {
     Spin
 } from "antd";
 import ProTable, {TableDropdown} from "@ant-design/pro-table";
-import {catchBlobReq, formatSize, orderCompare, post, preventClose, request, translate, waitTime} from "../utils/utils";
+import {catchBlobReq, formatSize, orderCompare, post, preventClose, request, waitTime} from "../utils/utils";
 import dayjs from "dayjs";
 import i18n from "../locale/locale";
 import {VList} from "virtuallist-antd";
diff --git a/web/src/components/generate.js b/web/src/components/generate.js
index 34566e6..e5b43ea 100644
--- a/web/src/components/generate.js
+++ b/web/src/components/generate.js
@@ -15,13 +15,11 @@ function Generate(props) {
         }
         form.secure = location.protocol === 'https:' ? 'true' : 'false';
         let basePath = location.origin + location.pathname + 'api/client/';
-        request(basePath + 'check', form)
-            .then((res) => {
-                if (res.data.code === 0) {
-                    post(basePath += 'generate', form);
-                }
-            })
-            .catch()
+        request(basePath + 'check', form).then(res => {
+            if (res.data.code === 0) {
+                post(basePath += 'generate', form);
+            }
+        }).catch();
     }
 
     function getInitValues() {
diff --git a/web/src/components/runner.js b/web/src/components/runner.js
new file mode 100644
index 0000000..b6779eb
--- /dev/null
+++ b/web/src/components/runner.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import {ModalForm, ProFormText} from '@ant-design/pro-form';
+import {request} from "../utils/utils";
+import i18n from "../locale/locale";
+import {message} from "antd";
+
+function Runner(props) {
+    async function onFinish(form) {
+        form.device = props.device.id;
+        let basePath = location.origin + location.pathname + 'api/device/';
+        request(basePath + 'exec', form).then(res => {
+            if (res.data.code === 0) {
+                message.success(i18n.t('executionSuccess'));
+            }
+        });
+    }
+
+    return (
+        <ModalForm
+            modalProps={{
+                destroyOnClose: true,
+                maskClosable: false,
+            }}
+            title={i18n.t('run')}
+            width={380}
+            onFinish={onFinish}
+            onVisibleChange={visible => {
+                if (!visible) props.onCancel();
+            }}
+            submitter={{
+                render: (_, elems) => elems.pop()
+            }}
+            {...props}
+        >
+            <ProFormText
+                width="md"
+                name="cmd"
+                label={i18n.t('cmdPlaceholder')}
+                rules={[{
+                    required: true
+                }]}
+            />
+            <ProFormText
+                width="md"
+                name="args"
+                label={i18n.t('argsPlaceholder')}
+            />
+        </ModalForm>
+    )
+}
+
+export default Runner;
\ No newline at end of file
diff --git a/web/src/locale/en.json b/web/src/locale/en.json
index 02356a0..4dc3bcc 100644
--- a/web/src/locale/en.json
+++ b/web/src/locale/en.json
@@ -25,6 +25,7 @@
 	"terminal": "Terminal",
 	"procMgr": "ProcMgr",
 	"fileMgr": "Explorer",
+	"run": "Run",
 	"screenshot": "Screenshot",
 	"desktop": "Desktop",
 	"lock": "Lock",
@@ -116,5 +117,9 @@
 	"screenshotObtainFailed": "Failed to obtain screenshot",
 	"noDisplayFound": "No display found",
 
+	"executionSuccess": "Execution success",
+	"cmdPlaceholder": "Command",
+	"argsPlaceholder": "Arguments (separated by space)",
+
 	"colon": ": "
 }
\ No newline at end of file
diff --git a/web/src/locale/zh-CN.json b/web/src/locale/zh-CN.json
index dd6d427..f18c2f5 100644
--- a/web/src/locale/zh-CN.json
+++ b/web/src/locale/zh-CN.json
@@ -25,6 +25,7 @@
 	"terminal": "终端",
 	"procMgr": "进程",
 	"fileMgr": "文件",
+	"run": "运行",
 	"screenshot": "截屏",
 	"desktop": "桌面",
 	"lock": "锁屏",
@@ -116,5 +117,9 @@
 	"screenshotObtainFailed": "截屏读取失败",
 	"noDisplayFound": "设备未连接显示器",
 
+	"executionSuccess": "执行成功",
+	"cmdPlaceholder": "命令",
+	"argsPlaceholder": "参数(以空格分隔)",
+
 	"colon": ":"
 }
\ No newline at end of file
diff --git a/web/src/pages/overview.js b/web/src/pages/overview.js
index 713cda5..49f8812 100644
--- a/web/src/pages/overview.js
+++ b/web/src/pages/overview.js
@@ -7,6 +7,7 @@ import Explorer from "../components/explorer";
 import Terminal from "../components/terminal";
 import ProcMgr from "../components/procmgr";
 import Desktop from "../components/desktop";
+import Runner from "../components/runner";
 import {QuestionCircleOutlined} from "@ant-design/icons";
 import i18n from "../locale/locale";
 
@@ -38,6 +39,7 @@ function UsageBar(props) {
 }
 
 function overview(props) {
+    const [runner, setRunner] = useState(false);
     const [desktop, setDesktop] = useState(false);
     const [procMgr, setProcMgr] = useState(false);
     const [explorer, setExplorer] = useState(false);
@@ -271,8 +273,9 @@ function overview(props) {
     }
     function renderOperation(device) {
         let menus = [
-            {key: 'screenshot', name: i18n.t('screenshot')},
+            {key: 'run', name: i18n.t('run')},
             {key: 'desktop', name: i18n.t('desktop')},
+            {key: 'screenshot', name: i18n.t('screenshot')},
             {key: 'lock', name: i18n.t('lock')},
             {key: 'logoff', name: i18n.t('logoff')},
             {key: 'hibernate', name: i18n.t('hibernate')},
@@ -299,6 +302,14 @@ function overview(props) {
     }
 
     function callDevice(act, device) {
+        if (act === 'run') {
+            setRunner(device);
+            return;
+        }
+        if (act === 'desktop') {
+            setDesktop(device);
+            return;
+        }
         if (act === 'screenshot') {
             request('/api/device/screenshot/get', {device: device.id}, {}, {
                 responseType: 'blob'
@@ -312,10 +323,6 @@ function overview(props) {
             }).catch(catchBlobReq);
             return;
         }
-        if (act === 'desktop') {
-            setDesktop(device);
-            return;
-        }
         Modal.confirm({
             title: i18n.t('operationConfirm').replace('{0}', i18n.t(act).toUpperCase()),
             icon: <QuestionCircleOutlined/>,
@@ -409,6 +416,11 @@ function overview(props) {
                 device={procMgr}
                 onCancel={setProcMgr.bind(null, false)}
             />
+            <Runner
+                visible={runner}
+                device={runner}
+                onCancel={setRunner.bind(null, false)}
+            />
             <Desktop
                 visible={desktop}
                 device={desktop}