fix: 处理容器日志导致的僵尸进程 (#2847)

Refs #2846
This commit is contained in:
ssongliu 2023-11-08 14:29:30 +08:00 committed by GitHub
parent 460687928a
commit e8ee373fc4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 53 additions and 70 deletions

View file

@ -434,7 +434,7 @@ func (b *BaseApi) ContainerLogs(c *gin.Context) {
follow := c.Query("follow") == "true"
tail := c.Query("tail")
if err := containerService.ContainerLogs(wsConn, container, since, tail, follow); err != nil {
if err := containerService.ContainerLogs(wsConn, "container", container, since, tail, follow); err != nil {
_ = wsConn.WriteMessage(1, []byte(err.Error()))
return
}
@ -658,7 +658,7 @@ func (b *BaseApi) ComposeLogs(c *gin.Context) {
follow := c.Query("follow") == "true"
tail := c.Query("tail")
if err := containerService.ComposeLogs(wsConn, compose, since, tail, follow); err != nil {
if err := containerService.ContainerLogs(wsConn, "compose", compose, since, tail, follow); err != nil {
_ = wsConn.WriteMessage(1, []byte(err.Error()))
return
}

View file

@ -13,6 +13,7 @@ import (
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/1Panel-dev/1Panel/backend/app/dto"
@ -55,7 +56,7 @@ type IContainerService interface {
ContainerRename(req dto.ContainerRename) error
ContainerLogClean(req dto.OperationWithName) error
ContainerOperation(req dto.ContainerOperation) error
ContainerLogs(wsConn *websocket.Conn, container, since, tail string, follow bool) error
ContainerLogs(wsConn *websocket.Conn, containerType, container, since, tail string, follow bool) error
ContainerStats(id string) (*dto.ContainerStats, error)
Inspect(req dto.InspectReq) (string, error)
DeleteNetwork(req dto.BatchDelete) error
@ -67,7 +68,6 @@ type IContainerService interface {
Prune(req dto.ContainerPrune) (dto.ContainerPruneReport, error)
LoadContainerLogs(req dto.OperationWithNameAndType) string
ComposeLogs(wsConn *websocket.Conn, composePath, since, tail string, follow bool) error
}
func NewIContainerService() IContainerService {
@ -592,11 +592,15 @@ func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
return nil
}
func (u *ContainerService) ContainerLogs(wsConn *websocket.Conn, container, since, tail string, follow bool) error {
func (u *ContainerService) ContainerLogs(wsConn *websocket.Conn, containerType, container, since, tail string, follow bool) error {
defer func() { wsConn.Close() }()
if cmd.CheckIllegal(container, since, tail) {
return buserr.New(constant.ErrCmdIllegal)
}
command := fmt.Sprintf("docker logs %s", container)
if containerType == "compose" {
command = fmt.Sprintf("docker-compose -f %s logs", container)
}
if tail != "0" {
command += " --tail " + tail
}
@ -608,6 +612,14 @@ func (u *ContainerService) ContainerLogs(wsConn *websocket.Conn, container, sinc
}
command += " 2>&1"
cmd := exec.Command("bash", "-c", command)
if !follow {
stdout, _ := cmd.CombinedOutput()
if err := wsConn.WriteMessage(websocket.TextMessage, stdout); err != nil {
global.LOG.Errorf("send message with log to ws failed, err: %v", err)
}
return nil
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
@ -615,23 +627,40 @@ func (u *ContainerService) ContainerLogs(wsConn *websocket.Conn, container, sinc
if err := cmd.Start(); err != nil {
return err
}
defer func() {
_ = cmd.Process.Signal(syscall.SIGTERM)
_ = cmd.Wait()
}()
var exitCh chan struct{}
if follow {
go func() {
_, wsData, _ := wsConn.ReadMessage()
if string(wsData) == "close conn" {
exitCh <- struct{}{}
}
}()
}
buffer := make([]byte, 1024)
for {
n, err := stdout.Read(buffer)
if err != nil {
if err == io.EOF {
break
select {
case <-exitCh:
return nil
default:
n, err := stdout.Read(buffer)
if err != nil {
if err == io.EOF {
return err
}
global.LOG.Errorf("read bytes from log failed, err: %v", err)
continue
}
if err = wsConn.WriteMessage(websocket.TextMessage, buffer[:n]); err != nil {
global.LOG.Errorf("send message with log to ws failed, err: %v", err)
return err
}
global.LOG.Errorf("read bytes from container log failed, err: %v", err)
continue
}
if err = wsConn.WriteMessage(websocket.TextMessage, buffer[:n]); err != nil {
global.LOG.Errorf("send message with container log to ws failed, err: %v", err)
break
}
}
return nil
}
func (u *ContainerService) ContainerStats(id string) (*dto.ContainerStats, error) {

View file

@ -4,7 +4,6 @@ import (
"bufio"
"errors"
"fmt"
"github.com/gorilla/websocket"
"io"
"os"
"os/exec"
@ -262,45 +261,3 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
}
return nil
}
func (u *ContainerService) ComposeLogs(wsConn *websocket.Conn, composePath, since, tail string, follow bool) error {
if cmd.CheckIllegal(composePath, since, tail) {
return buserr.New(constant.ErrCmdIllegal)
}
command := fmt.Sprintf("docker-compose -f %s logs", composePath)
if tail != "0" {
command += " --tail " + tail
}
if since != "all" {
command += " --since " + since
}
if follow {
command += " -f"
}
command += " 2>&1"
cmd := exec.Command("bash", "-c", command)
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
buffer := make([]byte, 1024)
for {
n, err := stdout.Read(buffer)
if err != nil {
if err == io.EOF {
break
}
global.LOG.Errorf("read bytes from compose log failed, err: %v", err)
continue
}
if err = wsConn.WriteMessage(websocket.TextMessage, buffer[:n]); err != nil {
global.LOG.Errorf("send message with compose log to ws failed, err: %v", err)
break
}
}
return nil
}

View file

@ -1,5 +1,5 @@
<template>
<el-drawer v-model="open" :size="globalStore.isFullScreen ? '100%' : '50%'">
<el-drawer v-model="open" :size="globalStore.isFullScreen ? '100%' : '50%'" :before-close="handleClose">
<template #header>
<DrawerHeader :header="$t('commons.button.log')" :resource="resource" :back="handleClose">
<template #extra v-if="!mobile">
@ -53,7 +53,7 @@
<script lang="ts" setup>
import i18n from '@/lang';
import { dateFormatForName, downloadWithContent } from '@/utils/util';
import { computed, onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue';
import { computed, reactive, ref, shallowRef, watch } from 'vue';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
@ -86,8 +86,8 @@ const logSearch = reactive({
});
const handleClose = () => {
terminalSocket.value!.send('close conn');
open.value = false;
terminalSocket.value?.close();
};
function toggleFullscreen() {
@ -180,10 +180,6 @@ const acceptParams = (props: DialogProps): void => {
}
};
onBeforeUnmount(() => {
terminalSocket.value?.close();
});
defineExpose({
acceptParams,
});

View file

@ -155,7 +155,7 @@ const onClean = async () => {
};
onBeforeUnmount(() => {
terminalSocket.value?.close();
terminalSocket.value!.send('close conn');
});
defineExpose({

View file

@ -4,6 +4,7 @@
v-model="logVisible"
:destroy-on-close="true"
:close-on-click-modal="false"
:before-close="handleClose"
:size="globalStore.isFullScreen ? '100%' : '50%'"
>
<template #header>
@ -58,7 +59,7 @@
/>
<template #footer>
<span class="dialog-footer">
<el-button @click="logVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
</span>
</template>
</el-drawer>
@ -131,8 +132,8 @@ const loadTooltip = () => {
return i18n.global.t('commons.button.' + (screenfull.isFullscreen ? 'quitFullscreen' : 'fullscreen'));
};
const handleClose = async () => {
terminalSocket.value!.send('close conn');
logVisible.value = false;
terminalSocket.value.close();
};
watch(logVisible, (val) => {
if (screenfull.isEnabled && !val && !mobile.value) screenfull.exit();
@ -208,7 +209,7 @@ const acceptParams = (props: DialogProps): void => {
};
onBeforeUnmount(() => {
terminalSocket.value?.close();
handleClose();
});
defineExpose({