Compare commits

..
Author SHA1 Message Date
ssongliu 5610c73d67 fix: Fix the problem that services exit unexpectedly when the contain… (#8127) 2025-03-12 03:19:03 +00:00
ssongliu 51fe02661f fix: Fix the problem of incorrect calculation of container memory res… (#8125)
Refs #8033
2025-03-12 02:13:03 +00:00
lm379andlm379 8f491b2933 feat: allow tencent cloud cos dual stack domain (#8123)
Co-authored-by: lm379 <lm379@qq.com>
2025-03-11 23:58:23 +08:00
王贺 b7e16bd6d5 Update .goreleaser.yaml (#8118) 2025-03-11 13:50:48 +08:00
ssongliu 38ca2ddd01 fix(container): Fix cpu surge when the container terminal executes exit (#8115) 2025-03-10 23:47:50 +08:00
ssongliu 402b41100b fix(container): Fix cpu surge when the container terminal executes exit (#8096)
Refs #7574
2025-03-07 11:06:59 +00:00
ssongliu 766beae4e1 fix(container): Fix the problem that a container cannot create a read… (#8095)
Refs #8074
2025-03-07 07:44:59 +00:00
maninhill 05f72e9dcf chore: Update README.md 2025-03-06 11:40:56 +08:00
igophper 23553c68ae fix(fail2ban): fix the issue of set fail2ban log path err (#8076)
Refs #8075
2025-03-06 09:44:48 +08:00
zhengkunwang 4dd1eb7a1e feat: Modify Swagger route response for unauthenticated access (#8057) 2025-03-04 05:48:57 +00:00
蘭 db307732c6 feat: Adjust license (#8056) 2025-03-03 14:32:56 +00:00
igophperandssongliu 33d4ade1d4 refactor(container): optimize memory calculation and concurrent data handling (#8038)
* refactor(container): optimize memory calculation and concurrent data handling

Refs #8033

* Update container.go

---------

Co-authored-by: ssongliu <73214554+ssongliu@users.noreply.github.com>
2025-03-03 17:38:32 +08:00
igophper 186075ff9a style: optimize code editor file path style (#8039)
Refs #8015
2025-03-03 10:23:24 +08:00
zhengkunwang 34df745d1c style: Change wget page style (#8016) 2025-02-26 10:16:54 +00:00
zhengkunwang ec363ac157 feat: update default app.yaml (#8011) 2025-02-26 15:35:54 +08:00
41 changed files with 260 additions and 285 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ builds:
- s390x
archives:
- format: tar.gz
- formats: [ 'tar.gz' ]
name_template: "1panel-v{{ .Version }}-{{ .Os }}-{{ .Arch }}{{- if .Arm }}v{{ .Arm }}{{ end }}"
wrap_in_directory: true
files:
+3 -1
View File
@@ -61,7 +61,9 @@ Please refer to our [documentation](https://docs.1panel.pro/quick_start/) for mo
## Pro Edition
Compared to the OSS Edition, 1Panel Pro Edition provides users with a wealth of enhanced features and technical support services. Enhanced features include WAF enhancement, website tamper protection, website monitoring, GPU monitoring, custom logo and theme color, etc. [Click to view the detailed introduction of the Pro Edition](https://1panel.pro/pricing).
Compared to the OSS Edition, 1Panel Pro Edition provides users with a wealth of enhanced features and technical support services. Enhanced features include WAF enhancement, Website monitoring, Mobile APP, custom logo and theme, etc.
[Click to see Pro Edition details](https://1panel.pro/pricing).
## Security Information
+68 -157
View File
@@ -10,7 +10,6 @@ import (
"time"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/service"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/copier"
@@ -74,7 +73,7 @@ func (b *BaseApi) WsSsh(c *gin.Context) {
}
}
func (b *BaseApi) RedisWsSsh(c *gin.Context) {
func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
@@ -96,193 +95,105 @@ func (b *BaseApi) RedisWsSsh(c *gin.Context) {
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
return
}
source := c.Query("source")
var containerID string
var initCmd string
switch source {
case "redis":
containerID, initCmd, err = loadRedisInitCmd(c)
case "ollama":
containerID, initCmd, err = loadOllamaInitCmd(c)
case "container":
containerID, initCmd, err = loadContainerInitCmd(c)
default:
if wshandleError(wsConn, fmt.Errorf("not support such source %s", source)) {
return
}
}
if wshandleError(wsConn, err) {
return
}
pidMap := loadMapFromDockerTop(containerID)
slave, err := terminal.NewCommand("clear && " + initCmd)
if wshandleError(wsConn, err) {
return
}
defer killBash(containerID, strings.ReplaceAll(initCmd, fmt.Sprintf("docker exec -it %s ", containerID), ""), pidMap)
defer slave.Close()
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
if wshandleError(wsConn, err) {
return
}
quitChan := make(chan bool, 3)
tty.Start(quitChan)
go slave.Wait(quitChan)
<-quitChan
global.LOG.Info("websocket finished")
if wshandleError(wsConn, err) {
return
}
}
func loadRedisInitCmd(c *gin.Context) (string, string, error) {
name := c.Query("name")
from := c.Query("from")
commands := []string{"redis-cli"}
commands := "redis-cli"
database, err := databaseService.Get(name)
if wshandleError(wsConn, errors.WithMessage(err, "no such database in db")) {
return
if err != nil {
return "", "", fmt.Errorf("no such database in db, err: %v", err)
}
if from == "local" {
redisInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: name, Type: "redis"})
if wshandleError(wsConn, errors.WithMessage(err, "no such database in db")) {
return
if err != nil {
return "", "", fmt.Errorf("no such app in db, err: %v", err)
}
name = redisInfo.ContainerName
if len(database.Password) != 0 {
commands = []string{"redis-cli", "-a", database.Password, "--no-auth-warning"}
commands = "redis-cli -a " + database.Password + " --no-auth-warning"
}
} else {
itemPort := fmt.Sprintf("%v", database.Port)
commands = []string{"redis-cli", "-h", database.Address, "-p", itemPort}
commands = fmt.Sprintf("redis-cli -h %s -p %v", database.Address, database.Port)
if len(database.Password) != 0 {
commands = []string{"redis-cli", "-h", database.Address, "-p", itemPort, "-a", database.Password, "--no-auth-warning"}
commands = fmt.Sprintf("redis-cli -h %s -p %v -a %s --no-auth-warning", database.Address, database.Port, database.Password)
}
name = "1Panel-redis-cli-tools"
}
pidMap := loadMapFromDockerTop(name)
itemCmds := append([]string{"exec", "-it", name}, commands...)
slave, err := terminal.NewCommand(itemCmds)
if wshandleError(wsConn, err) {
return
}
defer killBash(name, strings.Join(commands, " "), pidMap)
defer slave.Close()
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
if wshandleError(wsConn, err) {
return
}
quitChan := make(chan bool, 3)
tty.Start(quitChan)
go slave.Wait(quitChan)
<-quitChan
global.LOG.Info("websocket finished")
if wshandleError(wsConn, err) {
return
}
return name, fmt.Sprintf("docker exec -it %s %s", name, commands), nil
}
func (b *BaseApi) OllamaWsSsh(c *gin.Context) {
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
return
}
defer wsConn.Close()
if global.CONF.System.IsDemo {
if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) {
return
}
}
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) {
return
}
rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
return
}
func loadOllamaInitCmd(c *gin.Context) (string, string, error) {
name := c.Query("name")
if cmd.CheckIllegal(name) {
if wshandleError(wsConn, errors.New(" The command contains illegal characters.")) {
return
}
return "", "", fmt.Errorf("ollama model %s contains illegal characters", name)
}
container, err := service.LoadContainerName()
if wshandleError(wsConn, errors.WithMessage(err, " load container name for ollama failed")) {
return
}
commands := []string{"ollama", "run", name}
pidMap := loadMapFromDockerTop(container)
fmt.Println("pidMap")
for k, v := range pidMap {
fmt.Println(k, v)
}
itemCmds := append([]string{"exec", "-it", container}, commands...)
slave, err := terminal.NewCommand(itemCmds)
if wshandleError(wsConn, err) {
return
}
defer killBash(container, strings.Join(commands, " "), pidMap)
defer slave.Close()
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
if wshandleError(wsConn, err) {
return
}
quitChan := make(chan bool, 3)
tty.Start(quitChan)
go slave.Wait(quitChan)
<-quitChan
global.LOG.Info("websocket finished")
if wshandleError(wsConn, err) {
return
ollamaInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: "", Type: "ollama"})
if err != nil {
return "", "", fmt.Errorf("no such app in db, err: %v", err)
}
containerName := ollamaInfo.ContainerName
return containerName, fmt.Sprintf("docker exec -it %s ollama run %s", containerName, name), nil
}
func (b *BaseApi) ContainerWsSsh(c *gin.Context) {
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
return
}
defer wsConn.Close()
if global.CONF.System.IsDemo {
if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) {
return
}
}
func loadContainerInitCmd(c *gin.Context) (string, string, error) {
containerID := c.Query("containerid")
command := c.Query("command")
user := c.Query("user")
if len(command) == 0 || len(containerID) == 0 {
if wshandleError(wsConn, errors.New("error param of command or containerID")) {
return
}
}
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) {
return
}
rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
return
}
cmds := []string{"exec", containerID, command}
if len(user) != 0 {
cmds = []string{"exec", "-u", user, containerID, command}
}
if cmd.CheckIllegal(user, containerID, command) {
if wshandleError(wsConn, errors.New(" The command contains illegal characters.")) {
return
}
return "", "", fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID)
}
stdout, err := cmd.ExecWithCheck("docker", cmds...)
if wshandleError(wsConn, errors.WithMessage(err, stdout)) {
return
if len(command) == 0 || len(containerID) == 0 {
return "", "", fmt.Errorf("error param of command: %s or containerID: %s", command, containerID)
}
commands := []string{"exec", "-it", containerID, command}
command = fmt.Sprintf("docker exec -it %s %s", containerID, command)
if len(user) != 0 {
commands = []string{"exec", "-it", "-u", user, containerID, command}
}
pidMap := loadMapFromDockerTop(containerID)
slave, err := terminal.NewCommand(commands)
if wshandleError(wsConn, err) {
return
}
defer killBash(containerID, command, pidMap)
defer slave.Close()
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, true)
if wshandleError(wsConn, err) {
return
command = fmt.Sprintf("docker exec -it -u %s %s %s", user, containerID, command)
}
quitChan := make(chan bool, 3)
tty.Start(quitChan)
go slave.Wait(quitChan)
<-quitChan
global.LOG.Info("websocket finished")
if wshandleError(wsConn, err) {
return
}
return containerID, command, nil
}
func wshandleError(ws *websocket.Conn, err error) bool {
+1
View File
@@ -71,6 +71,7 @@ type SettingInfo struct {
ApiKey string `json:"apiKey"`
IpWhiteList string `json:"ipWhiteList"`
ApiKeyValidityTime string `json:"apiKeyValidityTime"`
LicenseVerify string `json:"licenseVerify"`
}
type SettingUpdate struct {
+24 -11
View File
@@ -255,14 +255,14 @@ func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error
if err != nil {
return nil, err
}
var datas []dto.ContainerListStats
datas := make([]dto.ContainerListStats, len(list))
var wg sync.WaitGroup
wg.Add(len(list))
for i := 0; i < len(list); i++ {
go func(item types.Container) {
datas = append(datas, loadCpuAndMem(client, item.ID))
go func(index int, item types.Container) {
datas[index] = loadCpuAndMem(client, item.ID)
wg.Done()
}(list[i])
}(i, list[i])
}
wg.Wait()
return datas, nil
@@ -937,15 +937,27 @@ func calculateCPUPercentUnix(stats *types.StatsJSON) float64 {
}
return cpuPercent
}
func calculateMemPercentUnix(memStats types.MemoryStats) float64 {
memPercent := 0.0
memUsage := float64(memStats.Usage)
memLimit := float64(memStats.Limit)
memUsage := calculateMemUsageUnixNoCache(memStats)
memLimit := memStats.Limit
if memUsage > 0.0 && memLimit > 0.0 {
memPercent = (memUsage / memLimit) * 100.0
memPercent = (float64(memUsage) / float64(memLimit)) * 100.0
}
return memPercent
}
func calculateMemUsageUnixNoCache(mem types.MemoryStats) uint64 {
if v, isCgroup1 := mem.Stats["total_inactive_file"]; isCgroup1 && v < mem.Usage {
return mem.Usage - v
}
if v := mem.Stats["inactive_file"]; v < mem.Usage {
return mem.Usage - v
}
return mem.Usage
}
func calculateBlockIO(blkio types.BlkioStats) (blkRead float64, blkWrite float64) {
for _, bioEntry := range blkio.IoServiceBytesRecursive {
switch strings.ToLower(bioEntry.Op) {
@@ -1063,7 +1075,7 @@ func loadCpuAndMem(client *client.Client, container string) dto.ContainerListSta
data.PercpuUsage = len(stats.CPUStats.CPUUsage.PercpuUsage)
data.MemoryCache = stats.MemoryStats.Stats["cache"]
data.MemoryUsage = stats.MemoryStats.Usage
data.MemoryUsage = calculateMemUsageUnixNoCache(stats.MemoryStats)
data.MemoryLimit = stats.MemoryStats.Limit
data.MemoryPercent = calculateMemPercentUnix(stats.MemoryStats)
@@ -1183,9 +1195,10 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *types
for _, volume := range req.Volumes {
if volume.Type == "volume" {
hostConf.Mounts = append(hostConf.Mounts, mount.Mount{
Type: mount.Type(volume.Type),
Source: volume.SourceDir,
Target: volume.ContainerDir,
Type: mount.Type(volume.Type),
Source: volume.SourceDir,
Target: volume.ContainerDir,
ReadOnly: volume.Mode == "ro",
})
config.Volumes[volume.ContainerDir] = struct{}{}
} else {
+1
View File
@@ -23,6 +23,7 @@ type System struct {
Language string `mapstructure:"language"`
IsDemo bool `mapstructure:"is_demo"`
IsIntl bool `mapstructure:"is_intl"`
LicenseVerify string `mapstructure:"license_verify"`
AppRepo string `mapstructure:"app_repo"`
ChangeUserInfo string `mapstructure:"change_user_info"`
OneDriveID string `mapstructure:"one_drive_id"`
+15
View File
@@ -84,6 +84,15 @@ func Init() {
global.CONF.System.ApiKeyValidityTime = apiKeyValidityTimeSetting.Value
}
if global.CONF.System.LicenseVerify == "" {
licenseVerify, err := settingRepo.Get(settingRepo.WithByKey("LicenseVerify"))
if err != nil {
global.LOG.Errorf("load service license verify from setting failed, err: %v", err)
}
global.CONF.System.LicenseVerify = licenseVerify.Value
}
handleLicenseVerify(global.CONF.System.LicenseVerify, settingRepo)
handleUserInfo(global.CONF.System.ChangeUserInfo, settingRepo)
handleCronjobStatus()
@@ -207,6 +216,12 @@ func loadLocalDir() {
global.LOG.Errorf("error type dir: %T", varMap["dir"])
}
func handleLicenseVerify(licenseVerify string, settingRepo repo.ISettingRepo) {
if err := settingRepo.Update("LicenseVerify", licenseVerify); err != nil {
global.LOG.Fatalf("init license verify before start failed, err: %v", err)
}
}
func handleUserInfo(tags string, settingRepo repo.ISettingRepo) {
if len(tags) == 0 {
return
+1
View File
@@ -105,6 +105,7 @@ func Init() {
migrations.AddOllamaModel,
migrations.AddAppMenu,
migrations.AddAppPanelName,
migrations.AddLicenseVerify,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
@@ -448,3 +448,14 @@ var AddAppPanelName = &gormigrate.Migration{
return nil
},
}
var AddLicenseVerify = &gormigrate.Migration{
ID: "20250226-add-license-verify",
Migrate: func(tx *gorm.DB) error {
if err := tx.Create(&model.Setting{Key: "LicenseVerify", Value: "LX"}).Error; err != nil {
return err
}
return nil
},
}
+6 -1
View File
@@ -188,7 +188,12 @@ func Routers() *gin.Engine {
swaggerRouter := Router.Group("1panel")
docs.SwaggerInfo.BasePath = "/api/v1"
swaggerRouter.Use(middleware.JwtAuth()).Use(middleware.SessionAuth()).GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
swaggerRouter.GET("/swagger/*any", func(c *gin.Context) {
if !checkSession(c) {
handleNoRoute(c)
return
}
}, ginSwagger.WrapHandler(swaggerfiles.Handler))
PublicGroup := Router.Group("")
{
PublicGroup.GET("/health", func(c *gin.Context) {
+2
View File
@@ -3,6 +3,7 @@ package viper
import (
"bytes"
"fmt"
"os"
"path"
"strings"
@@ -103,6 +104,7 @@ func Init() {
global.CONF.System.Entrance = entrance
global.CONF.System.Language = language
global.CONF.System.ChangeUserInfo = loadChangeInfo()
global.CONF.System.LicenseVerify = os.Getenv("LXWARE_LICENSE_VERIFY")
global.Viper = v
}
-1
View File
@@ -15,7 +15,6 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
baseApi := v1.ApiGroupApp.BaseApi
{
aiToolsRouter.GET("/ollama/exec", baseApi.OllamaWsSsh)
aiToolsRouter.POST("/ollama/close", baseApi.CloseOllamaModel)
aiToolsRouter.POST("/ollama/model", baseApi.CreateOllamaModel)
aiToolsRouter.POST("/ollama/model/recreate", baseApi.RecreateOllamaModel)
+1 -1
View File
@@ -15,7 +15,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
Use(middleware.PasswordExpired())
baseApi := v1.ApiGroupApp.BaseApi
{
baRouter.GET("/exec", baseApi.ContainerWsSsh)
baRouter.GET("/exec", baseApi.ContainerWsSSH)
baRouter.GET("/stats/:id", baseApi.ContainerStats)
baRouter.POST("", baseApi.ContainerCreate)
-1
View File
@@ -38,7 +38,6 @@ func (s *DatabaseRouter) InitRouter(Router *gin.RouterGroup) {
cmdRouter.POST("/redis/persistence/conf", baseApi.LoadPersistenceConf)
cmdRouter.POST("/redis/status", baseApi.LoadRedisStatus)
cmdRouter.POST("/redis/conf", baseApi.LoadRedisConf)
cmdRouter.GET("/redis/exec", baseApi.RedisWsSsh)
cmdRouter.GET("/redis/check", baseApi.CheckHasCli)
cmdRouter.POST("/redis/install/cli", baseApi.InstallCli)
cmdRouter.POST("/redis/password", baseApi.ChangeRedisPassword)
+1 -1
View File
@@ -47,9 +47,9 @@ func Start() {
session.Init()
gin.SetMode(gin.DebugMode)
cron.Run()
hook.Init()
InitOthers()
business.Init()
hook.Init()
rootRouter := router.Routers()
+13 -2
View File
@@ -6,6 +6,7 @@ import (
"net/http"
"net/url"
"os"
"regexp"
cosSDK "github.com/tencentyun/cos-go-sdk-v5"
)
@@ -18,6 +19,7 @@ type cosClient struct {
func NewCosClient(vars map[string]interface{}) (*cosClient, error) {
region := loadParamFromVars("region", vars)
endpoint := loadParamFromVars("endpoint", vars)
accessKey := loadParamFromVars("accessKey", vars)
secretKey := loadParamFromVars("secretKey", vars)
bucket := loadParamFromVars("bucket", vars)
@@ -26,7 +28,16 @@ func NewCosClient(vars map[string]interface{}) (*cosClient, error) {
scType = "Standard"
}
u, _ := url.Parse(fmt.Sprintf("https://cos.%s.myqcloud.com", region))
// 允许使用对象存储双栈域名
endpointType := "cos"
if len(endpoint) != 0 {
re := regexp.MustCompile(`.*cos-dualstack\..*`)
if re.MatchString(endpoint) {
endpointType = "cos-dualstack"
}
}
u, _ := url.Parse(fmt.Sprintf("https://%s.%s.myqcloud.com", endpointType, region))
b := &cosSDK.BaseURL{BucketURL: u}
client := cosSDK.NewClient(b, &http.Client{
Transport: &cosSDK.AuthorizationTransport{
@@ -36,7 +47,7 @@ func NewCosClient(vars map[string]interface{}) (*cosClient, error) {
})
if len(bucket) != 0 {
u2, _ := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com", bucket, region))
u2, _ := url.Parse(fmt.Sprintf("https://%s.%s.%s.myqcloud.com", bucket, endpointType, region))
b2 := &cosSDK.BaseURL{BucketURL: u2}
clientWithBucket := cosSDK.NewClient(b2, &http.Client{
Transport: &cosSDK.AuthorizationTransport{
-55
View File
@@ -1,11 +1,8 @@
package ssh
import (
"bytes"
"fmt"
"io"
"strings"
"sync"
"time"
gossh "golang.org/x/crypto/ssh"
@@ -82,58 +79,6 @@ func (c *ConnInfo) Close() {
_ = c.Client.Close()
}
type SshConn struct {
StdinPipe io.WriteCloser
ComboOutput *wsBufferWriter
Session *gossh.Session
}
func (c *ConnInfo) NewSshConn(cols, rows int) (*SshConn, error) {
sshSession, err := c.Client.NewSession()
if err != nil {
return nil, err
}
stdinP, err := sshSession.StdinPipe()
if err != nil {
return nil, err
}
comboWriter := new(wsBufferWriter)
sshSession.Stdout = comboWriter
sshSession.Stderr = comboWriter
modes := gossh.TerminalModes{
gossh.ECHO: 1,
gossh.TTY_OP_ISPEED: 14400,
gossh.TTY_OP_OSPEED: 14400,
}
if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil {
return nil, err
}
if err := sshSession.Shell(); err != nil {
return nil, err
}
return &SshConn{StdinPipe: stdinP, ComboOutput: comboWriter, Session: sshSession}, nil
}
func (s *SshConn) Close() {
if s.Session != nil {
s.Session.Close()
}
}
type wsBufferWriter struct {
buffer bytes.Buffer
mu sync.Mutex
}
func (w *wsBufferWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
return w.buffer.Write(p)
}
func makePrivateKeySigner(privateKey []byte, passPhrase []byte) (gossh.Signer, error) {
if len(passPhrase) != 0 {
return gossh.ParsePrivateKeyWithPassphrase(privateKey, passPhrase)
+12 -2
View File
@@ -25,14 +25,24 @@ type LocalCommand struct {
pty *os.File
}
func NewCommand(commands []string) (*LocalCommand, error) {
cmd := exec.Command("docker", commands...)
func NewCommand(initCmd string) (*LocalCommand, error) {
cmd := exec.Command("bash")
if term := os.Getenv("TERM"); term != "" {
cmd.Env = append(os.Environ(), "TERM="+term)
} else {
cmd.Env = append(os.Environ(), "TERM=xterm")
}
pty, err := pty.Start(cmd)
if err != nil {
return nil, errors.Wrapf(err, "failed to start command")
}
if len(initCmd) != 0 {
time.Sleep(100 * time.Millisecond)
_, _ = pty.Write([]byte(initCmd + "\n"))
}
lcmd := &LocalCommand{
closeSignal: DefaultCloseSignal,
closeTimeout: DefaultCloseTimeout,
@@ -53,6 +53,11 @@ func (sws *LocalWsSession) handleSlaveEvent(exitCh chan bool) {
}
func (sws *LocalWsSession) masterWrite(data []byte) error {
defer func() {
if r := recover(); r != nil {
global.LOG.Errorf("A panic occurred during write ws message to master, error message: %v", r)
}
}()
sws.writeMutex.Lock()
defer sws.writeMutex.Unlock()
wsData, err := json.Marshal(WsMsg{
@@ -72,6 +77,7 @@ func (sws *LocalWsSession) masterWrite(data []byte) error {
func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) {
defer func() {
if r := recover(); r != nil {
setQuit(exitCh)
global.LOG.Errorf("A panic occurred during receive ws message, error message: %v", r)
}
}()
+2 -1
View File
@@ -5,7 +5,8 @@ system:
repo_url: https://resource.1panel.pro
app_repo: https://apps.1panel.pro
is_demo: false
port: 8888
is_intl: true
port: 9999
username: admin
password: admin123
+1
View File
@@ -62,6 +62,7 @@ export namespace Setting {
apiKey: string;
ipWhiteList: string;
apiKeyValidityTime: number;
licenseVerify: string;
}
export interface SettingUpdate {
key: string;
-2
View File
@@ -123,8 +123,6 @@ const message = {
},
msg: {
noneData: 'No data available',
disConn:
'Please click the disconnect button directly to terminate the terminal connection, avoiding the use of exit commands like {0}.',
delete: `This operation delete can't be undone. Do you want to continue?`,
clean: `This operation clean can't be undone. Do you want to continue?`,
deleteTitle: 'Delete',
-2
View File
@@ -122,8 +122,6 @@ const message = {
},
msg: {
noneData: '利用可能なデータはありません',
disConn:
'端末接続を切断するには、{0} のような終了コマンドを使用せずに、直接切断ボタンをクリックしてください',
delete: `この操作削除は元に戻すことはできません。続けたいですか?`,
clean: `この操作は取り消すことはできません。続けたいですか?`,
deleteTitle: '消去',
-2
View File
@@ -123,8 +123,6 @@ const message = {
},
msg: {
noneData: '데이터가 없습니다',
disConn:
'종료 명령어인 {0} 등을 사용하지 않고 직접 연결 끊기 버튼을 클릭하여 터미널 연결을 종료해 주십시오.',
delete: `이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?`,
clean: `이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?`,
deleteTitle: '삭제',
-2
View File
@@ -123,8 +123,6 @@ const message = {
},
msg: {
noneData: 'Tiada data tersedia',
disConn:
'Sila klik butang putus sambungan secara langsung untuk menamatkan sambungan terminal, mengelakkan penggunaan arahan keluar seperti {0}.',
delete: 'Operasi ini tidak boleh diundur. Adakah anda mahu meneruskan?',
clean: 'Operasi ini tidak boleh diundur. Adakah anda mahu meneruskan?',
deleteTitle: 'Padam',
-2
View File
@@ -123,8 +123,6 @@ const message = {
},
msg: {
noneData: 'Nenhum dado disponível',
disConn:
'Por favor, clique diretamente no botão de desconexão para encerrar a conexão do terminal, evitando o uso de comandos de saída como {0}.',
delete: 'Esta operação de exclusão não pode ser desfeita. Deseja continuar?',
clean: 'Esta operação de limpeza não pode ser desfeita. Deseja continuar?',
deleteTitle: 'Excluir',
-2
View File
@@ -124,8 +124,6 @@ const message = {
},
msg: {
noneData: 'Нет данных',
disConn:
'Пожалуйста, нажмите кнопку отключения, чтобы разорвать соединение с терминалом, избегая использования команд выхода, таких как {0}.',
delete: 'Эта операция удаления не может быть отменена. Хотите продолжить?',
clean: 'Эта операция очистки не может быть отменена. Хотите продолжить?',
deleteTitle: 'Удалить',
-1
View File
@@ -123,7 +123,6 @@ const message = {
},
msg: {
noneData: '暫無資料',
disConn: '請直接點選斷開按鈕斷開終端連接,避免使用 {0} 等退出指令。',
delete: '刪除 操作不可復原,是否繼續?',
clean: '清空 操作不可復原,是否繼續?',
deleteTitle: '刪除',
-1
View File
@@ -121,7 +121,6 @@ const message = {
Rollbacking: '快照回滚中,请稍候...',
},
msg: {
disConn: '请直接点击断开按钮断开终端连接,避免使用 {0} 等退出命令',
noneData: '暂无数据',
delete: '删除 操作不可回滚,是否继续?',
clean: '清空 操作不可回滚,是否继续?',
+1
View File
@@ -37,6 +37,7 @@ export interface GlobalState {
isIntl: boolean;
isTrial: boolean;
productProExpires: number;
licenseVerify: string;
errStatus: string;
}
+1
View File
@@ -40,6 +40,7 @@ const GlobalStore = defineStore({
isIntl: false,
isTrial: false,
productProExpires: 0,
licenseVerify: '',
errStatus: '',
}),
@@ -16,12 +16,7 @@
</template>
</DrawerHeader>
</template>
<el-alert type="error" :closable="false">
<template #title>
<span>{{ $t('commons.msg.disConn', ['/bye exit']) }}</span>
</template>
</el-alert>
<Terminal class="mt-2" style="height: calc(100vh - 225px)" ref="terminalRef"></Terminal>
<Terminal class="mt-2" style="height: calc(100vh - 175px)" ref="terminalRef"></Terminal>
<template #footer>
<span class="dialog-footer">
@@ -67,8 +62,8 @@ const loadTooltip = () => {
const initTerm = () => {
nextTick(() => {
terminalRef.value.acceptParams({
endpoint: '/api/v1/ai/ollama/exec',
args: `name=${itemName.value}`,
endpoint: '/api/v1/containers/exec',
args: `source=ollama&name=${itemName.value}`,
error: '',
initCmd: '',
});
@@ -10,12 +10,7 @@
<template #header>
<DrawerHeader :header="$t('container.containerTerminal')" :resource="title" :back="handleClose" />
</template>
<el-alert type="error" :closable="false">
<template #title>
<span>{{ $t('commons.msg.disConn', ['exit']) }}</span>
</template>
</el-alert>
<el-form ref="formRef" class="mt-2" :model="form" label-position="top">
<el-form ref="formRef" :model="form" label-position="top">
<el-form-item :label="$t('commons.table.user')" prop="user">
<el-input placeholder="root" clearable v-model="form.user" />
</el-form-item>
@@ -51,7 +46,7 @@
</el-button>
<el-button v-else @click="onClose()">{{ $t('commons.button.disconnect') }}</el-button>
<Terminal
style="height: calc(100vh - 355px); margin-top: 18px"
style="height: calc(100vh - 312px); margin-top: 18px"
ref="terminalRef"
v-if="terminalOpen"
></Terminal>
@@ -104,7 +99,7 @@ const initTerm = (formEl: FormInstance | undefined) => {
await nextTick();
terminalRef.value!.acceptParams({
endpoint: '/api/v1/containers/exec',
args: `containerid=${form.containerID}&user=${form.user}&command=${form.command}`,
args: `source=container&containerid=${form.containerID}&user=${form.user}&command=${form.command}`,
error: '',
initCmd: '',
});
+4 -4
View File
@@ -309,8 +309,8 @@ const initTerminal = async () => {
terminalShow.value = true;
redisStatus.value = 'Running';
terminalRef.value.acceptParams({
endpoint: '/api/v1/databases/redis/exec',
args: `name=${currentDBName.value}&from=${currentDB.value.from}`,
endpoint: '/api/v1/containers/exec',
args: `source=redis&name=${currentDBName.value}&from=${currentDB.value.from}`,
error: '',
initCmd: '',
});
@@ -327,8 +327,8 @@ const initTerminal = async () => {
if (res.data.status === 'Running') {
terminalShow.value = true;
terminalRef.value.acceptParams({
endpoint: '/api/v1/databases/redis/exec',
args: `name=${currentDBName.value}&from=${currentDB.value.from}`,
endpoint: '/api/v1/containers/exec',
args: `source=redis&name=${currentDBName.value}&from=${currentDB.value.from}`,
error: '',
initCmd: '',
});
@@ -11,8 +11,10 @@
>
<template #header>
<div ref="dialogHeader" class="flex items-center justify-between">
<span>{{ $t('commons.button.edit') + ' - ' + form.path }}</span>
<el-space alignment="center" :size="1" class="dialog-header-icon">
<div class="truncate flex-1 mr-2">
<span>{{ $t('commons.button.edit') + ' - ' + form.path }}</span>
</div>
<el-space alignment="center" :size="1" class="dialog-header-icon flex-shrink-0">
<el-tooltip :content="loadTooltip()" placement="top">
<el-button
@click="toggleFullscreen"
@@ -6,15 +6,47 @@
:before-close="handleClose"
:title="$t('file.downloadProcess')"
>
<div v-for="(value, index) in res" :key="index">
<span>{{ value['percent'] === 100 ? $t('file.downloadSuccess') : $t('file.downloading') }}</span>
<MsgInfo :info="value['name']" width="250" />
<el-progress v-if="value['total'] == 0" :percentage="100" :indeterminate="true" :duration="1" />
<el-progress v-else :text-inside="true" :stroke-width="15" :percentage="value['percent']"></el-progress>
<span>
{{ getFileSize(value['written']) }}/
<span v-if="value['total'] > 0">{{ getFileSize(value['total']) }}</span>
</span>
<div class="space-y-4 p-4">
<div
v-for="(value, index) in res"
:key="index"
class="bg-white rounded-lg p-4 shadow-sm border border-gray-100 transition-all duration-200 hover:shadow-md"
:class="{ completed: value.percent === 100 }"
>
<div class="flex items-center gap-3">
<div class="flex-1">
<MsgInfo :info="value.name" class="text-gray-700" />
<div class="text-gray-500">
{{ value.percent === 100 ? $t('file.downloadSuccess') : $t('file.downloading') }}
</div>
</div>
</div>
<div class="space-y-2">
<div class="flex justify-end text-gray-500 mb-1">
<span>{{ getFileSize(value.written) }}</span>
<span v-if="value.total > 0" class="text-gray-400">/{{ getFileSize(value.total) }}</span>
</div>
<div class="w-full">
<el-progress
v-if="value.total === 0 && value.percent != 100"
:percentage="100"
:indeterminate="true"
:duration="1"
class="progress-bar"
:stroke-width="8"
:show-text="false"
/>
<el-progress
v-else
:percentage="value.percent"
:stroke-width="8"
class="progress-bar"
:status="value.percent === 100 ? 'success' : ''"
/>
</div>
</div>
</div>
</div>
</el-dialog>
</template>
@@ -108,3 +140,30 @@ const onOpen = () => {
getKeys();
};
</script>
<style type="scss" scoped>
.download-item.completed {
@apply bg-green-50/50;
}
.progress-bar {
:deep(.el-progress-bar__outer) {
@apply rounded-full bg-gray-100;
}
:deep(.el-progress-bar__inner) {
@apply rounded-full transition-all duration-300;
}
}
@keyframes bounce {
0%,
100% {
transform: translateY(-10%);
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
}
50% {
transform: translateY(0);
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
}
</style>
@@ -412,6 +412,7 @@ const loadDataFromDB = async () => {
i18n.locale.value = res.data.language;
i18n.warnHtmlMessage = false;
globalStore.entrance = res.data.securityEntrance;
globalStore.licenseVerify = res.data.licenseVerify;
globalStore.setDefaultNetwork(res.data.defaultNetwork);
globalStore.setOpenMenuTabs(res.data.menuTabs === 'enable');
globalStore.updateLanguage(res.data.language);
+1 -1
View File
@@ -77,7 +77,7 @@
<el-col :xs="24" :sm="24" :md="9" :lg="9" :xl="9">
<CardWithHeader :header="$t('license.quickUpdate')" height="160px">
<template #body>
<div class="h-app-card">
<div class="h-app-card" v-if="globalStore.licenseVerify != 'TC' || !hasLicense">
<el-row>
<el-col :span="15">
<div class="h-app-content">{{ $t('license.importLicense') }}</div>
@@ -84,7 +84,7 @@ const onSave = async (formEl: FormInstance | undefined) => {
type: 'info',
},
).then(async () => {
await updateFail2ban({ key: 'logPath', value: form.logPath })
await updateFail2ban({ key: 'logpath', value: form.logPath })
.then(async () => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
loading.value = false;
+2 -2
View File
@@ -58,6 +58,7 @@ require (
golang.org/x/sys v0.29.0
golang.org/x/term v0.28.0
golang.org/x/text v0.21.0
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38
gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.25.7
@@ -218,7 +219,7 @@ require (
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1105 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
@@ -258,7 +259,6 @@ require (
golang.org/x/sync v0.10.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.28.0 // indirect
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/grpc v1.67.1 // indirect
+2 -1
View File
@@ -961,8 +961,9 @@ github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRd
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065 h1:krcqtAmexnHHBm/4ge4tr2b1cn/a7JGBESVGoZYXQAE=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1105 h1:lCs0dmezU6/8JcfNwEaam1Pm1RS/5MhXvNhf/X2y65s=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1105/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 h1:aEFtLD1ceyeljQXB1S2BjN0zjTkf0X3XmpuxFIiC29w=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065/go.mod h1:HWvwy09hFSMXrj9SMvVRWV4U7rZO3l+WuogyNuxiT3M=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0=