基于AIOHTTP、Websocket和Vue3一步步实现web部署平台,无延迟控制台输出,接近原生SSH连接

发布于:2025-02-13 ⋅ 阅读:(16) ⋅ 点赞:(0)

背景:笔者是一名Javaer,但是最近因为某些原因迷上了Python和它的Asyncio,至于什么原因?请往下看。在着迷”犯浑“的过程中,也接触到了一些高并发高性能的组件,通过简单的学习和了解,aiohttp这个组件引起了我极大的兴趣。

协程、异步非阻塞、”吓人“的性能,这些关键词让我不得不注意到它。

老样子,我们先看成品,再讲讲我曲折的过程。

读取实时日志

 构建+部署

技术痛点揭露

相信大家一定遇到过笔者这次的场景:

        疫情隔离,居家办公,这次我们做的是一个小程序,前端的小伙伴们要联调接口,可是不能用公司的资源,因为公司都断电了😭 ,于是乎我自己买(bai)了(piao)云服务器,自己搭建了一套环境,用自己的域名给他们架上了。本以为事情解决了,前后端可以愉快地调试接口了,但是想都别想,现实还是无情地用它宽大的手掌啪啪打我的三寸小脸。

        你看,后端的小伙伴写完代码,改完bug,提交了之后,一次又一次的让你部署,导致吃饭都想着部署,每次都是噼里啪啦一堆命令,脑瓜子嗡嗡的。(First Blood!)

        你再看,后端猿小帅和前端媛小美正在对接接口,小美说接口怎么一直报错?小帅眉头一皱,手一抖,微信窗口多出个小表情,一脸无辜:"我本地可以啊!"。对,就是这句话,“我本地可以啊,为啥线上不行”,成了接口对接中的口头禅。完了,肯定又是我的活,这不,咔咔一顿"艾特",让我帮忙看日志,啊啊啊啊啊啊,一天到晚登上服务器看了不下N次日志,我的头发在抗议。(Double Kill!)

        你再再再看,我们对接用的yapi,在接口未完成之前,前端调用的是mock,完成之后,得切换到真实接口。为了保证项目开发进度,让前后端的联调顺滑如丝,那付出的肯定是我了。一天下来,在忙上面事情的同时,我还在不断地调整Nginx反向代理配置,为他们放开一个个接口的代理。我内心只能说:mmp。(Triple Kill!)

        你再再再再看看,正常开发过程中总有些粗心捣蛋的人,提交的代码像一个炸弹。这不,刚刚这哥们还在小区楼底下蹦迪,下一秒回家晕乎乎地写了几个bug,潇洒提交,又蹦迪去了。这不提交不要紧,一提交之后,紧接着我习惯性的部署上去,一系列的连锁反应导致几个接口不能用了,兄弟们叫苦不迭,要不是居家,我真想上去抽那仁兄几个嘴巴子。这屁股还是得我擦,回退到上个版本,先凑合调试着。这种操作隔三差五在上演,也是麻烦的很。。。(Quadra kill!)

        最后脑补一个五杀(Penta kill!)🧠

尝试曲线救国

        上面列举了那么多痛点,是个人都被折磨的够呛吧,拜托🙏🏻,疫情即使在家办公也是要高效,更何况家人都在身边,不能焦躁,不能焦躁,不能焦躁!

        这个时候就有大佬说了,你搞这么多费力不讨好的事情,为啥不直接用CI/CD(持续集成)呢?我花费了5根头发想了想,我这1GB内存,1核CPU还能再战吗?再摸了摸我那比纸都薄的钱包,最后点了三炷香“祭奠”了一下我死去的5根头发,心里默默说了声,算了,忍忍,你可以的。

方案一   脚本大法 + 代理

        我开始尝试写脚本。我们的项目是微服务,正常部署都应该用docker-compose,或者直接上到k8s集群里,但是非常时期我们没有办法,只能人工部署。所以我写了一个又一个的脚本,然后写好备注,然后写了一个Low到爆的 HTML,写了超级烂的几行Java代码来调用这些脚本。最后通过Nginx给他们代理出去,把URL分发出去让他们自己点。

        我花费了几个小时完成了上述工作,就在我以为万事大吉的时候,我发现我服务器进不去了。。。WTF?登上控制台,看到CPU使用率125%?我就一个核怎么还超过100捏?虚机超频?呸呸呸,言归正传,排查了半天,我发现是因为多个人短时间内执行构建脚本和部署脚本,直接启动多个进程把机器“干”死了,我摇了摇头,方案1?去你的吧。

方案二  方案一的“进化”

        鉴于方案一存在的致命短板,我不得不针对这个问题进行优化,优化的手段嘛,不出大家所料,还是脚本,用low到爆的一个办法:每次运行构建,都通过 ps | grep | xargs kill -9 杀死之前的进程,再进行构建。运行结果也增加了反馈,用户执行结果会根据Sheel执行返回值进行判断,给出成功与否的响应。至于触发方法嘛,当然是老样子, 继续HTML点击,Java调用脚本。

        再次试验效果,我组织了一场视频会议,会议上我让小美和阿伟还有阿强分别点击部署,哇,效果嘎(chao)嘎(la)的(ji),小美先点击的居然部署成功了,阿伟和阿强后来的居然被杀了?awsl(阿伟死了)。后来排查了半天,发现小美家的wifi只有一格信号,请求发到后台慢了。总体来说方案二的可用度提高了,但是依然没什么卵用,小美提交的代码运行一会后报错了,原因是阿伟提交的一段代码引用了jdk中sun包内的东西,服务器openjdk没有相关类,服务压根没起来。还是很鸡肋。

方案三 另辟蹊径

        方案一和方案二都是短时间内拍脑门儿想出来的活,到现在为止我已经发现问题不能这么草率的解决了, 否则永远都是不断地返工。我深刻地分析了一下,作为一个完备的协同部署功能,至少需要满足以下几个条件:

1. 能够协同工作和实时交互。看了比较大的运维平台,基本上都具备实时的反馈,接近SSH会话级别的体验,能够确认当前的部署状态和部署进度,用户可以及时发现并避免和其他人的交叉使用。此外,如果可能的话,应该实现当前部署状态未完成,其他用户不可操作服务器。

2. 能够查看实时日志。系统运行的状况如何,应该具备日志查看的入口,这些入口开放给开发人员,才能够做到每个人都能及时处理自己的问题。此外,日志滚动频率过快,应该提供“暂停日志”和“恢复日志”的能力。

3. 实现用户身份标识。该功能也是必须的,因为通过HTML按钮点击部署出了问题,往往无法追溯是谁干的😭。后面,我设计为每个用户提供身份标识确认,通过线下发放key的方式提供服务的使用权限,每个key可以绑定到具体的用户,绑定key后,才可以正常使用运维能力。

4. 具有版本控制和一键回滚。一个合格的部署平台,必须具有防范风险的能力,体现在健壮性上来说,就是版本控制。利用shell脚本实现版本控制并不难,实现一键回滚也不难,难的是库表结构修改后产生的种种恩怨情仇。

        经过系统的分析之后,我们说干就干。

= 开始干活 =

        工欲善其事,必先利其器。干活前老样子,先做技术选型。为了一步到位,我直接选择了Vue3去写前端,后端压根没想着用Java去写,因为我写过很多pipleline的代码,java处理起来冗长又效率低下,果断选择了Python大法。事实证明,我的选择太明智了。

搭建Vue项目

技术栈

  • Vue 3
  • Ant Design Vue 3.1.1
  • Socket.io-client
  • CodeMirror Editor
  • Axios

我们使用最新的vue-cli搭建项目。

1. 环境准备

# 安装 Vue CLI
npm install -g @vue/cli

# 创建项目
vue create web

# 安装依赖
cd web
yarn add ant-design-vue @ant-design/icons-vue axios socket.io-client codemirror-editor-vue3

2. 项目配置

babel.config.js - 按需加载配置
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  plugins: [
    ["import", { "libraryName": "ant-design-vue", "libraryDirectory": "es", "style": "css" }]
  ]
}
vue.config.js - 开发服务器配置
module.exports = defineConfig({
  transpileDependencies: true,
  devServer: {
    port: 7777,
    proxy: {
      '/api': {
        target: 'http://localhost:8090',
        ws: false,
        changeOrigin: true,
      },
    }
  },
})

2. 核心功能实现

1. WebSocket 通信模块

项目使用 Socket.io 实现与后端的实时通信:

const wsConnect = (write, done) => {
  const handler = (data) => {
    // 处理管道数据
    const { success, end, content, msg = '' } = data
    if (success) {
      content && write(content.replaceAll('\0', ' '))
      end && write('\n任务执行完毕', done)
    } else {
      write('\n管道读取失败!' + msg, done)
    }
  }

  return {
    io: null,
    async connect() {
      this.io = io(WS_URL, {
        transports: ['websocket'],
        query: { token }
      }).on('pipeline', this.handler.bind(this))
    },
    
    // 发送请求
    request(event, data = {}) {
      return new Promise((resolve, reject) => {
        const rid = Date.now()
        this.io.emit(event, { rid, ...data })
        this.pending[rid] = { resolve, reject }
      })
    }
  }
}
2. 部署控制台实现
<template>
  <div class="deploy">
    <a-card :body-style="{padding: '10px 24px'}">
      <div class="opt-group">
        构建和部署:
        <a-button v-if="!deploying" :disabled="running" class="primary" type="primary" @click="deploy">
          <template #icon>
            <build-filled />
          </template>
          构建并部署
        </a-button>
        <a-button v-else :loading="stopping" class="primary" type="danger" @click="stop">
          <template #icon>
            <close-circle-filled />
          </template>
          停止部署
        </a-button>
        <a-button :disabled="deploying || running" class="primary" type="primary" @click="web">
          <template #icon>
            <global-outlined />
          </template>
          构建部署前端
        </a-button>
        <a-dropdown-button :disabled="deploying || running" type="danger" @click="restore" @visibleChange="loadHistories">
          <hourglass-filled />回滚版本
          <template #overlay>
            <a-menu @click="editFile">
              <template v-if="histories.length">
                <a-menu-item :key="file" v-for="file in histories">{{file}}</a-menu-item>
              </template>
              <a-menu-item v-else disabled key="more">暂无可回滚版本</a-menu-item>
            </a-menu>
          </template>
        </a-dropdown-button>
      </div>
      <a-divider class="divider" type="vertical" />
      <div class="opt-group">
        运行监控:
        <a-button v-if="!running" :disabled="deploying" class="primary" type="primary" @click="log">
          <template #icon>
            <snippets-filled />
          </template>
          读取运行日志
        </a-button>
        <a-button v-else :loading="stopping" class="primary" type="danger" @click="stop">
          <template #icon>
            <close-circle-filled />
          </template>
          停止日志读取
        </a-button>
        <a-button :disabled="deploying || stopping || running" :loading="restarting" class="primary" type="danger"
                  @click="restart">
          <template #icon>
            <appstore-filled />
          </template>
          重启项目
        </a-button>
        <a-button v-if="!paused" :disabled="!running" @click="pause">
          <template #icon>
            <pause-circle-filled />
          </template>
          暂停日志
        </a-button>
        <a-button v-else :disabled="!running" type="primary" @click="play">
          <template #icon>
            <play-circle-filled />
          </template>
          恢复日志
        </a-button>
      </div>
      <template v-if="admin">
        <a-divider class="divider" type="vertical" />
        <div class="opt-group">
          配置维护:
          <a-button class="primary" type="primary" @click="editFile">
            <template #icon>
              <setting-filled />
            </template>
            修改配置文件
          </a-button>
          <a-dropdown trigger="click">
            <template #overlay>
              <a-menu @click="editFile">
                <a-menu-item :key="file" v-for="file in files">{{file}}</a-menu-item>
                <a-menu-item key="more">创建脚本...</a-menu-item>
              </a-menu>
            </template>
            <a-button @click="loadFiles">
              修改项目脚本
              <DownOutlined />
            </a-button>
          </a-dropdown>
        </div>
      </template>
    </a-card>
    <a-card>
      <template #extra><a href="#">当前版本v1.5.3</a></template>
      <template #title>
        <code-filled style="margin-right: 10px" />
        控制台
        <a-divider type="vertical" />
        <a v-if="current === 'deploy'">当前:部署日志</a>
        <a v-else>当前:运行日志</a>
      </template>
      <code-mirror ref="editorRef" :height="350" :options="cmOptions" class="console" />
    </a-card>
    <a-drawer
        width="1000"
        :visible="!!editing.key"
        title="修改文件内容"
        placement="right"
    >
      <code-mirror height="100%" :options="cmOptions" v-model:value="editing.content" class="console" />
      <template #footer>
        <div style="text-align: center">
          <a-button style="margin-right: 8px" @click="editing = {content: ''}">取消</a-button>
          <a-button type="primary" :loading="editing.loading" @click="saveFile">保存</a-button>
        </div>
      </template>
    </a-drawer>
  </div>
</template>

<script>
import { onMounted, ref } from 'vue';
import CodeMirror from 'codemirror-editor-vue3';
import { message, Modal } from 'ant-design-vue';
import {
  AppstoreFilled,
  BuildFilled,
  CloseCircleFilled,
  CodeFilled,
  DownOutlined,
  GlobalOutlined,
  HourglassFilled,
  PauseCircleFilled,
  PlayCircleFilled,
  SettingFilled,
  SnippetsFilled,
} from '@ant-design/icons-vue';
// import base style
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/material-darker.css'
// language
import 'codemirror/mode/javascript/javascript.js';
import createSocket from '@/api/pipeline';

export default {
  name: 'DeployPage',
  components: {
    CodeMirror,
    CodeFilled,
    GlobalOutlined,
    BuildFilled,
    SettingFilled,
    HourglassFilled,
    SnippetsFilled,
    PauseCircleFilled,
    PlayCircleFilled,
    CloseCircleFilled,
    AppstoreFilled,
    DownOutlined
  },
  // 在我们的组件中
  setup() {
    // socketio
    const socket = ref(null);
    // 编辑器实例
    const editorRef = ref(null);
    const editor = ref(null);
    // 活跃的回调
    const callback = ref(null);
    // 日志运行状态
    const running = ref(false);
    // 日志暂停状态
    const paused = ref(false);
    // 部署运行状态
    const deploying = ref(false);
    // 重启运行状态
    const restarting = ref(false);
    // 通用停止按钮状态
    const stopping = ref(false);
    // 配置文件列表
    const files = ref([]);
    // 历史版本列表
    const histories = ref([]);
    // 当前控制台视图,支持deploy部署、log日志
    const current = ref('deploy');
    // 运行管道信息
    const runningKey = ref('');
    // 缓存内容
    const cache = ref('');
    // 当前编辑文件
    const editing = ref({});
    // 目标对应字段
    const dicts = {
      set deploy(value) {
        deploying.value = value;
      },
      get deploy() {
        return deploying.value;
      },
      set web(value) {
        deploying.value = value;
      },
      get web() {
        return deploying.value;
      },
      set log(value) {
        running.value = value;
      },
      get log() {
        return running.value;
      },
      set restart(value) {
        restarting.value = value;
        running.value = value;
        if (!value) {
          appender('\n已完成重启,请读取日志查看')
        }
      },
      get restart() {
        return restarting.value;
      },
      set restore(value) {
        deploying.value = value;
      },
      get restore() {
        return deploying.value;
      },
    };
    // 日志追加器
    const appender = (text, end, clear) => {
      if (clear) {
        return editor.value?.setValue(text || '')
      }
      if (text) {
        // 如果暂停了,进缓存
        if (paused.value) {
          cache.value += text;
        } else {
          editor.value?.replaceRange(text, { line: Infinity });
          editor.value?.scrollTo(0, Infinity);
        }
      }
      // 具有回调,代表结束,做一些重置
      if (end) {
        end();
        cache.value = '';
        paused.value = false;
        restarting.value = false;
        runningKey.value = '';
      }
    };
    // 建立pipeline并读取
    const connector = async (target) => {
      current.value = target;
      callback.value = error => {
        if (error) appender('\n连接中断或异常,' + error);
        dicts[target] = false;
      };
      try {
        dicts[target] = true;
        runningKey.value = await socket.value.open(target);
        appender('\n管道建立成功!进程id:' + runningKey.value + '\n');
      } catch (e) {
        appender('\n无法建立管道连接,' + e.message, callback);
      }
    }
    // 挂载后获取实例
    onMounted(async () => {
      editor.value = editorRef.value?.cminstance;
      socket.value = await createSocket(appender, callback).connect();
      const instance = editor.value;
      if (instance) {
        instance.setValue('暂无运行日志\n\n\n\n\n\n\n\n\n\n\n\n\n');
        instance.focus();
      }
    });
    // 返回命名空间
    return {
      editorRef,
      running,
      stopping,
      current,
      deploying,
      restarting,
      paused,
      files,
      histories,
      editing,
      get admin() {
        return socket.value?.admin;
      },
      play: () => {
        paused.value = false;
        const cached = cache.value;
        cache.value = ''
        appender(cached);
      },
      pause: () => paused.value = true,
      deploy: async () => connector('deploy'),
      log: async () => connector('log'),
      restart: async () => connector('restart'),
      web: async () => connector('web'),
      // 停止pipeline并清理
      stop: async () => {
        try {
          stopping.value = true;
          await socket.value.kill(runningKey.value);
          appender('\n成功发送杀死指令')
        } catch (e) {
          appender('\n杀死作业失败!' + e.message)
        } finally {
          stopping.value = false;
        }
      },
      restore: () => {
        Modal.confirm({
          title: '请确认操作',
          content: '该操作会将上次运行的构建结果替换到当前环境运行,并且不可撤销,请确认操作',
          okText: '我确定',
          cancelText: '还是不了',
          onOk: async () => connector('restore'),
        })
      },
      loadFiles: async () => {
        files.value = await socket.value.listFile();
      },
      loadHistories: async visible => {
        try {
          histories.value = visible ? await socket.value.listHistory() : [];
        } catch (e) {
          message.error(e.message);
        }
      },
      editFile: async ({ key }) => {
        try {
          const body = { key };
          if (!key) {
            Object.assign(body, await socket.value.createFile('config.json'))
          } else if (key === 'more') {
            Object.assign(body, await socket.value.createFile())
          } else {
            body.content = await socket.value.getFile(key)
          }
          editing.value = body;
        } catch (e) {
          message.warn(e.message || e);
        }
      },
      saveFile: async () => {
        const close = message.loading('正在保存中...', 0);
        try {
          editing.value.loading = true;
          const { key, content } = editing.value;
          await socket.value.saveFile(key, content);
          editing.value = { content: '' };
          message.success('保存成功!')
        } catch (e) {
          message.error(e.message);
        } finally {
          close();
          editing.value.loading = false;
        }
      },
      cmOptions: {
        mode: "text/javascript", // Language mode
        theme: "material-darker", // Theme
        lineNumbers: true, // Show line number
        smartIndent: true, // Smart indent
        viewportMargin: 350,
        indentUnit: 2, // The smart indent unit is 2 spaces in length
        foldGutter: true, // Code folding
        styleActiveLine: true, // Display the style of the selected row
      },
    }
  },
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}

ul {
  list-style-type: none;
  padding: 0;
}

li {
  display: inline-block;
  margin: 0 10px;
}

a {
  color: #42b983;
}

.primary {
  margin-right: 20px
}

.console {

}

.divider {
  margin: 0 15px;
}

.opt-group {
  display: inline-block;
  line-height: 50px;
}

@media screen and (max-width: 954px) {
  .opt-group {
    display: block;
  }

  .divider {
    display: none;
  }
}
</style>
3. 项目结构
web/
├── src/
│   ├── components/    # 组件
│   │   └── Deploy.vue # 部署控制台组件
│   ├── App.vue        # 根组件
│   └── main.js        # 入口文件
├── public/
│   └── banner.jpg     # 静态资源
├── babel.config.js    # babel配置
└── vue.config.js      # Vue CLI配置

至此,我们实现了:

  • 实时部署状态监控
  • 运行日志实时查看
  • 配置文件在线编辑
  • 版本回滚功能
  • 项目重启功能

总结一下,这波操作采用 Vue 3 + Ant Design Vue 的技术栈,实现了一个功能完整的智能部署控制台。通过 WebSocket 实现了与后端的实时通信,使用 CodeMirror 提供了良好的代码编辑体验。

Python异步部署服务端

经过技术的吸收,我实现了基于Python 3.9+的异步部署工具,主要特点:

  • 基于WebSocket的全双工实时通信
  • 支持自定义部署脚本
  • 支持配置热加载
  • 支持多用户管理
  • 支持部署历史版本管理

技术栈

  • Python 3.9+

  • aiohttp - 异步Web框架

  • python-socketio - WebSocket库

  • SQLite3 - 轻量级数据库

  • watchdog - 文件监控

核心实现

1. WebSocket服务器

使用python-socketio实现WebSocket服务器:

# 初始化socketio服务器
sio = socketio.AsyncServer(async_mode='aiohttp',
                           cors_allowed_origins=['http://localhost:7777', 'http://deploy.flyfish.group'])
app = web.Application()
sio.attach(app)

# 处理连接事件
@sio.event
async def connect(sid, environ):
    user = validate_token(environ['aiohttp.request'], sid)
    # 缓存客户端
    clients[sid] = {'process': None, 'killed': False, 'name': user['name']}
    await send(sid, {'success': True, 'user': {'name': user['name'], 'authority': user['authorities']}})

# 处理断开事件
@sio.event 
async def disconnect(sid):
    if sid in clients:
        process = clients[sid]['process']
        if process:
            await kill_pipeline(sid, {'pid': process.pid})
        del clients[sid]
2. 异步管道实现

使用asyncio.create_subprocess_shell创建子进程,实现命令执行:

# 打开管道
@sio.event
async def open_pipeline(sid, message):
    # 取得类型和命令
    pipe_type = message['type']
    command = configs['scripts'][pipe_type]
    
    # 启动子进程
    proc = await asyncio.create_subprocess_shell(
        f'cd {configs["work_dir"]} && {command} {client["name"]}',
        stdout=asyncio.subprocess.PIPE,
        preexec_fn=os.setsid
    )
    
    # 返回成功
    await send(sid, {'success': True, 'pid': proc.pid, 'rid': message['rid']})
    # 等待提交
    await submit(sid, proc)

# 异步读取输出
async def submit(sid, proc):
    item = clients[sid]
    item['process'] = proc
    
    while True:
        # 异步读取输出
        line = await proc.stdout.read(BLOCK_SIZE)
        if not line:
            break
        # 实时推送到客户端    
        await send(sid, {
            'success': True, 
            'content': str(line, encoding='utf-8')
        })
3. 配置热加载

使用watchdog监控配置文件变化:

# 配置文件监听器
class ConfigFileHandler(FileSystemEventHandler):
    def on_modified(self, event):
        path = event.src_path
        if path.endswith('config.json'):
            print("修改了配置文件,尝试加载...")
            if load_config():
                print('😊配置文件已经重载')

# 初始化监听
async def init_app():
    observer = Observer()
    observer.schedule(ConfigFileHandler(), './')
    observer.start()
    load_config()
    return app
4. 数据库操作封装

使用上下文管理器封装SQLite操作:

class SqlSession:
    def execute(self, sql, param=()):
        with self.conn:
            cursor = self.conn.cursor()
            try:
                return cursor.execute(sql, param)
            except sqlite3.Error as e:
                cursor.close()
                raise e

class SqlOperation:
    # 插入操作
    def insert(self, data):
        if 'id' in data:
            del data['id']
        data['create_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        keys = data.keys()
        values = data.values()
        sql = f'insert into {self.table} ({",".join(keys)}) values ({",".join(["?"] * len(keys))})'
        self.session.execute(sql, tuple(values))
5. Webhook实现

实现Gitee的Webhook接收,能够自动部署,持续集成:

@app.route('/deploy', methods=['POST'])
def post_data():
    # 验证token
    token = request.headers.get('X-Gitee-Token')
    if token != gitee_secret:
        return "token认证无效", 401
        
    # 获取推送信息
    data = json.loads(request.data)
    name = data['pusher']['name']
    
    # 执行部署脚本
    os.system(f'sh deploy.sh {name}')
    return jsonify({"status": 200})

项目结构

hooks/
├── bin/                # 核心代码
│   ├── app.py         # WebSocket服务器
│   ├── db.py          # 数据库操作
│   ├── hook.py        # Webhook接收器
│   └── post.py        # 消息推送
├── logs/              # 日志目录
└── requirements.txt   # 依赖配置

总结

到此,我们实现了以下所有能力

1. 全异步通信

  • 使用aiohttp和python-socketio实现全双工通信
  • 异步子进程管理,实时输出
  • 支持多用户并发操作
  • 实时配置
  • 配置文件热加载
  • 支持自定义部署脚本
  • 支持工作目录配置
  • 用户管理
  • 基于Token的认证
  • 会话管理
  • 权限控制

4. 部署管理

  • 支持部署历史
  • 支持版本回滚
  • 支持运行日志查看

通过以上努力,我采用Python异步编程实现了一个功能完整的部署工具,通过WebSocket实现了与前端的实时通信,支持多用户并发操作。

结束语 - 让部署不再是996的理由 🚀

写在最后

各位看官读到这里,相信你已经发现这不是一个普通的部署工具,而是一个能让你告别"部署恐惧症"的神器!

从此告别的场景 😂

  • 再也不用半夜被运维电话叫醒:"服务器挂了!"
  • 不用每次部署都像在玩俄罗斯轮盘赌
  • 告别"在我电脑上能运行"系列尴尬
  • 不用为搞错配置而痛哭流涕

你将收获的快乐 🎉

  • 一键部署,比订外卖还快
  • 实时日志,像看抖音一样上瘾
  • 版本回滚,时光机般的存在
  • 配置热加载,改完配置说走就走

写给犹豫的你 🤔

如果你还在为以下问题困扰:

  • 部署靠"祈祷"
  • 改配置要"跪求"
  • 看日志要"冥想"
  • 回滚要"许愿"

那么,来试试这个工具吧!它不仅能让你的部署工作变得轻松愉快,还能让你在同事面前装个小小的技术大佬。😎

彩蛋时间 🎮

知道为什么我们选择 WebSocket 吗?

  • 因为 HTTP 太慢了,慢得像极了周一的早晨
  • 因为实时通信,快得像极了发工资的瞬间
  • 因为全双工通信,比你谈恋爱还要双向奔赴

最后的最后 🌟

记住,这个工具的诞生不是为了让你加班,而是为了让你有更多时间:

  • 摸鱼 🐟
  • 追剧 📺
  • 打游戏 🎮
  • 谈恋爱 💑

如果这个项目帮你节省了时间,别忘了给我们点个星⭐️

如果没帮你节省时间...那一定是你还没用熟练 😅

愿你的每一次部署,都像喝可乐一样爽快!

愿你的每一次发版,都像春游一样愉快!

愿你的每一次回滚,都像退货一样简单!

结语中的结语 📝

记住我们的口号:

> 部署不再难,生活更自然!

>

> 配置不用愁,周末早回家!

>

> 日志一目了然,Bug无处遁藏!

最后送大家一句话:

> 工具再好,也补不了你的bug!

>

> 但至少...它能让你改bug的时候心情好一点!😊

好了,快去试试吧!让我们一起告别996,拥抱995.9!🎯


注:本项目副作用可能包括但不限于:让你对其他部署工具产生严重的依赖性鄙视,让你的同事对你投来羡慕的眼光,让你的老板觉得你太闲需要安排更多任务... 😜

代码下载

🎉 是的!我们开源啦!

💝 为什么要开源?

因为我们相信:

  • 好的代码应该像老婆的美貌一样,值得炫耀
  • 优秀的项目应该像奶茶一样,值得分享
  • 牛逼的工具应该像八卦一样,让更多人知道

下载地址奉上,希望大家支持!开发不易,请尊重博主的劳动成果!

https://download.csdn.net/download/wybaby168/90373568