fix issue

pull/369/head
vapao 2021-08-12 23:46:54 +08:00
parent 8a5c61b841
commit 8365365854
13 changed files with 212 additions and 173 deletions

View File

@ -158,14 +158,14 @@ def _deploy_ext1_host(req, helper, h_id, env):
if not host: if not host:
helper.send_error(h_id, 'no such host') helper.send_error(h_id, 'no such host')
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname}) env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
ssh = host.get_ssh() with host.get_ssh(default_env=env) as ssh:
code, _ = ssh.exec_command( code, _ = ssh.exec_command_raw(
f'mkdir -p {extend.dst_repo} && [ -e {extend.dst_dir} ] && [ ! -L {extend.dst_dir} ]') f'mkdir -p {extend.dst_repo} && [ -e {extend.dst_dir} ] && [ ! -L {extend.dst_dir} ]')
if code == 0: if code == 0:
helper.send_error(host.id, f'检测到该主机的发布目录 {extend.dst_dir!r} 已存在为了数据安全请自行备份后删除该目录Spug 将会创建并接管该目录。') helper.send_error(host.id, f'检测到该主机的发布目录 {extend.dst_dir!r} 已存在为了数据安全请自行备份后删除该目录Spug 将会创建并接管该目录。')
# clean # clean
clean_command = f'ls -d {extend.deploy_id}_* 2> /dev/null | sort -t _ -rnk2 | tail -n +{extend.versions + 1} | xargs rm -rf' clean_command = f'ls -d {extend.deploy_id}_* 2> /dev/null | sort -t _ -rnk2 | tail -n +{extend.versions + 1} | xargs rm -rf'
helper.remote(host.id, ssh, f'cd {extend.dst_repo} && rm -rf {req.spug_version} && {clean_command}') helper.remote_raw(host.id, ssh, f'cd {extend.dst_repo} && rm -rf {req.spug_version} && {clean_command}')
# transfer files # transfer files
tar_gz_file = f'{req.spug_version}.tar.gz' tar_gz_file = f'{req.spug_version}.tar.gz'
try: try:
@ -174,7 +174,7 @@ def _deploy_ext1_host(req, helper, h_id, env):
helper.send_error(host.id, f'exception: {e}') helper.send_error(host.id, f'exception: {e}')
command = f'cd {extend.dst_repo} && tar xf {tar_gz_file} && rm -f {req.deploy_id}_*.tar.gz' command = f'cd {extend.dst_repo} && tar xf {tar_gz_file} && rm -f {req.deploy_id}_*.tar.gz'
helper.remote(host.id, ssh, command) helper.remote_raw(host.id, ssh, command)
helper.send_step(h_id, 1, '完成\r\n') helper.send_step(h_id, 1, '完成\r\n')
# pre host # pre host
@ -182,18 +182,18 @@ def _deploy_ext1_host(req, helper, h_id, env):
if extend.hook_pre_host: if extend.hook_pre_host:
helper.send_step(h_id, 2, f'{human_time()} 发布前任务... \r\n') helper.send_step(h_id, 2, f'{human_time()} 发布前任务... \r\n')
command = f'cd {repo_dir} ; {extend.hook_pre_host}' command = f'cd {repo_dir} ; {extend.hook_pre_host}'
helper.remote(host.id, ssh, command, env) helper.remote(host.id, ssh, command)
# do deploy # do deploy
helper.send_step(h_id, 3, f'{human_time()} 执行发布... ') helper.send_step(h_id, 3, f'{human_time()} 执行发布... ')
helper.remote(host.id, ssh, f'rm -f {extend.dst_dir} && ln -sfn {repo_dir} {extend.dst_dir}') helper.remote_raw(host.id, ssh, f'rm -f {extend.dst_dir} && ln -sfn {repo_dir} {extend.dst_dir}')
helper.send_step(h_id, 3, '完成\r\n') helper.send_step(h_id, 3, '完成\r\n')
# post host # post host
if extend.hook_post_host: if extend.hook_post_host:
helper.send_step(h_id, 4, f'{human_time()} 发布后任务... \r\n') helper.send_step(h_id, 4, f'{human_time()} 发布后任务... \r\n')
command = f'cd {extend.dst_dir} ; {extend.hook_post_host}' command = f'cd {extend.dst_dir} ; {extend.hook_post_host}'
helper.remote(host.id, ssh, command, env) helper.remote(host.id, ssh, command)
helper.send_step(h_id, 100, f'\r\n{human_time()} ** 发布成功 **') helper.send_step(h_id, 100, f'\r\n{human_time()} ** 发布成功 **')
@ -204,7 +204,7 @@ def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
if not host: if not host:
helper.send_error(h_id, 'no such host') helper.send_error(h_id, 'no such host')
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname}) env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
ssh = host.get_ssh() with host.get_ssh(default_env=env) as ssh:
for index, action in enumerate(actions): for index, action in enumerate(actions):
helper.send_step(h_id, 1 + index, f'{human_time()} {action["title"]}...\r\n') helper.send_step(h_id, 1 + index, f'{human_time()} {action["title"]}...\r\n')
if action.get('type') == 'transfer': if action.get('type') == 'transfer':
@ -228,7 +228,7 @@ def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
command += f'&& rm -rf /tmp/{spug_version}* && echo "transfer completed"' command += f'&& rm -rf /tmp/{spug_version}* && echo "transfer completed"'
else: else:
command = f'cd /tmp ; {action["data"]}' command = f'cd /tmp ; {action["data"]}'
helper.remote(host.id, ssh, command, env) helper.remote(host.id, ssh, command)
helper.send_step(h_id, 100, f'\r\n{human_time()} ** 发布成功 **') helper.send_step(h_id, 100, f'\r\n{human_time()} ** 发布成功 **')
@ -400,7 +400,6 @@ class Helper:
if env: if env:
env = dict(env.items()) env = dict(env.items())
env.update(os.environ) env.update(os.environ)
command = 'set -e\n' + command
task = subprocess.Popen(command, env=env, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) task = subprocess.Popen(command, env=env, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while True: while True:
message = task.stdout.readline() message = task.stdout.readline()
@ -416,3 +415,8 @@ class Helper:
self.send_info(key, out) self.send_info(key, out)
if code != 0: if code != 0:
self.send_error(key, f'exit code: {code}') self.send_error(key, f'exit code: {code}')
def remote_raw(self, key, ssh, command):
code, out = ssh.exec_command_raw(command)
if code != 0:
self.send_error(key, f'exit code: {code}')

View File

@ -124,10 +124,10 @@ class RequestDetailView(View):
if not req: if not req:
return json_response(error='未找到指定发布申请') return json_response(error='未找到指定发布申请')
hosts = Host.objects.filter(id__in=json.loads(req.host_ids)) hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
outputs = {x.id: {'id': x.id, 'title': x.name, 'data': [f'{human_time()} 读取数据... ']} for x in hosts} outputs = {x.id: {'id': x.id, 'title': x.name, 'data': f'{human_time()} 读取数据... '} for x in hosts}
response = {'outputs': outputs, 'status': req.status} response = {'outputs': outputs, 'status': req.status}
if req.deploy.extend == '2': if req.deploy.extend == '2':
outputs['local'] = {'id': 'local', 'data': [f'{human_time()} 读取数据... ']} outputs['local'] = {'id': 'local', 'data': f'{human_time()} 读取数据... '}
response['s_actions'] = json.loads(req.deploy.extend_obj.server_actions) response['s_actions'] = json.loads(req.deploy.extend_obj.server_actions)
response['h_actions'] = json.loads(req.deploy.extend_obj.host_actions) response['h_actions'] = json.loads(req.deploy.extend_obj.host_actions)
if not response['h_actions']: if not response['h_actions']:
@ -139,7 +139,7 @@ class RequestDetailView(View):
for item in data: for item in data:
item = json.loads(item.decode()) item = json.loads(item.decode())
if 'data' in item: if 'data' in item:
outputs[item['key']]['data'].append(item['data']) outputs[item['key']]['data'] += item['data']
if 'step' in item: if 'step' in item:
outputs[item['key']]['step'] = item['step'] outputs[item['key']]['step'] = item['step']
if 'status' in item: if 'status' in item:
@ -160,7 +160,7 @@ class RequestDetailView(View):
return json_response(error='该申请单当前状态还不能执行发布') return json_response(error='该申请单当前状态还不能执行发布')
hosts = Host.objects.filter(id__in=json.loads(req.host_ids)) hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
message = f'{human_time()} 等待调度... ' message = f'{human_time()} 等待调度... '
outputs = {x.id: {'id': x.id, 'title': x.name, 'step': 0, 'data': [message]} for x in hosts} outputs = {x.id: {'id': x.id, 'title': x.name, 'step': 0, 'data': message} for x in hosts}
req.status = '2' req.status = '2'
req.do_at = human_datetime() req.do_at = human_datetime()
req.do_by = request.user req.do_by = request.user
@ -168,7 +168,7 @@ class RequestDetailView(View):
Thread(target=dispatch, args=(req,)).start() Thread(target=dispatch, args=(req,)).start()
if req.deploy.extend == '2': if req.deploy.extend == '2':
message = f'{human_time()} 建立连接... ' message = f'{human_time()} 建立连接... '
outputs['local'] = {'id': 'local', 'step': 0, 'data': [message]} outputs['local'] = {'id': 'local', 'step': 0, 'data': message}
s_actions = json.loads(req.deploy.extend_obj.server_actions) s_actions = json.loads(req.deploy.extend_obj.server_actions)
h_actions = json.loads(req.deploy.extend_obj.host_actions) h_actions = json.loads(req.deploy.extend_obj.host_actions)
if not h_actions: if not h_actions:

View File

@ -39,7 +39,7 @@ class Job:
def run(self): def run(self):
if not self.token: if not self.token:
with self.ssh: with self.ssh:
return self.ssh.exec_command_raw(self.command) return self.ssh.exec_command(self.command)
self.send('\x1b[36m### Executing ...\x1b[0m\r') self.send('\x1b[36m### Executing ...\x1b[0m\r')
code = -1 code = -1
try: try:

View File

@ -24,9 +24,9 @@ class Host(models.Model, ModelMixin):
def private_key(self): def private_key(self):
return self.pkey or AppSetting.get('private_key') return self.pkey or AppSetting.get('private_key')
def get_ssh(self, pkey=None): def get_ssh(self, pkey=None, default_env=None):
pkey = pkey or self.private_key pkey = pkey or self.private_key
return SSH(self.hostname, self.port, self.username, pkey) return SSH(self.hostname, self.port, self.username, pkey, default_env=default_env)
def to_view(self): def to_view(self):
tmp = self.to_dict() tmp = self.to_dict()

View File

@ -189,7 +189,8 @@ def fetch_host_extend(ssh):
"fdisk -l 2> /dev/null | grep '^Disk /' | awk '{print $5}'", "fdisk -l 2> /dev/null | grep '^Disk /' | awk '{print $5}'",
"fdisk -l 2> /dev/null | grep '^磁盘 /' | awk '{print $4}' | awk -F'' '{print $2}'" "fdisk -l 2> /dev/null | grep '^磁盘 /' | awk '{print $4}' | awk -F'' '{print $2}'"
] ]
code, out = ssh.exec_command(';'.join(commands)) with ssh:
code, out = ssh.exec_command_raw(';'.join(commands))
if code != 0: if code != 0:
raise Exception(out) raise Exception(out)
response = {'disk': [], 'public_ip_address': [], 'private_ip_address': []} response = {'disk': [], 'public_ip_address': [], 'private_ip_address': []}
@ -252,11 +253,11 @@ def _sync_host_extend(host, private_key=None, public_key=None, password=None, ss
def _get_ssh(kwargs, pkey=None, private_key=None, public_key=None, password=None): def _get_ssh(kwargs, pkey=None, private_key=None, public_key=None, password=None):
try: try:
ssh = SSH(pkey=pkey or private_key, **kwargs) ssh = SSH(pkey=pkey or private_key, **kwargs)
ssh.ping() ssh.get_client()
return ssh return ssh
except AuthenticationException as e: except AuthenticationException as e:
if password: if password:
ssh = SSH(password=str(password), **kwargs) with SSH(password=str(password), **kwargs) as ssh:
ssh.add_public_key(public_key) ssh.add_public_key(public_key)
return _get_ssh(kwargs, private_key) return _get_ssh(kwargs, private_key)
raise e raise e

View File

@ -56,8 +56,8 @@ def ping_check(addr):
def host_executor(host, command): def host_executor(host, command):
try: try:
cli = host.get_ssh() with host.get_ssh() as ssh:
exit_code, out = cli.exec_command(command) exit_code, out = ssh.exec_command(command)
if exit_code == 0: if exit_code == 0:
return True, out or '检测状态正常' return True, out or '检测状态正常'
else: else:

View File

@ -28,8 +28,8 @@ def local_executor(command):
def host_executor(host, command): def host_executor(host, command):
code, out, now = 1, None, time.time() code, out, now = 1, None, time.time()
try: try:
cli = host.get_ssh() with host.get_ssh() as ssh:
code, out = cli.exec_command(command) code, out = ssh.exec_command(command)
except AuthenticationException: except AuthenticationException:
out = 'ssh authentication fail' out = 'ssh authentication fail'
except socket.error as e: except socket.error as e:

View File

@ -10,13 +10,15 @@ import re
class SSH: class SSH:
def __init__(self, hostname, port=22, username='root', pkey=None, password=None, connect_timeout=10): def __init__(self, hostname, port=22, username='root', pkey=None, password=None, default_env=None,
connect_timeout=10):
self.stdout = None self.stdout = None
self.client = None self.client = None
self.channel = None self.channel = None
self.sftp = None self.sftp = None
self.eof = 'Spug EOF 2108111926' self.eof = 'Spug EOF 2108111926'
self.regex = re.compile(r'Spug EOF 2108111926 \d+[\r\n]?$') self.default_env = self._make_env_command(default_env)
self.regex = re.compile(r'Spug EOF 2108111926 -?\d+[\r\n]?$')
self.arguments = { self.arguments = {
'hostname': hostname, 'hostname': hostname,
'port': port, 'port': port,
@ -36,22 +38,20 @@ class SSH:
def get_client(self): def get_client(self):
if self.client is not None: if self.client is not None:
return self.client return self.client
print('\n~~ ssh start ~~')
self.client = SSHClient() self.client = SSHClient()
self.client.set_missing_host_key_policy(AutoAddPolicy) self.client.set_missing_host_key_policy(AutoAddPolicy)
self.client.connect(**self.arguments) self.client.connect(**self.arguments)
return self.client return self.client
def ping(self): def ping(self):
self.get_client()
return True return True
def add_public_key(self, public_key): def add_public_key(self, public_key):
command = f'mkdir -p -m 700 ~/.ssh && \ command = f'mkdir -p -m 700 ~/.ssh && \
echo {public_key!r} >> ~/.ssh/authorized_keys && \ echo {public_key!r} >> ~/.ssh/authorized_keys && \
chmod 600 ~/.ssh/authorized_keys' chmod 600 ~/.ssh/authorized_keys'
_, out, _ = self.client.exec_command(command) exit_code, out = self.exec_command_raw(command)
if out.channel.recv_exit_status() != 0: if exit_code != 0:
raise Exception(f'add public key error: {out}') raise Exception(f'add public key error: {out}')
def exec_command_raw(self, command): def exec_command_raw(self, command):
@ -69,7 +69,7 @@ class SSH:
channel.send(command) channel.send(command)
out, exit_code = '', -1 out, exit_code = '', -1
for line in self.stdout: for line in self.stdout:
if line.startswith(self.eof): if self.regex.search(line):
exit_code = int(line.rsplit()[-1]) exit_code = int(line.rsplit()[-1])
break break
out += line out += line
@ -91,10 +91,6 @@ class SSH:
yield exit_code, line yield exit_code, line
yield exit_code, line yield exit_code, line
def get_file(self, file):
sftp = self._get_sftp()
return sftp.open(file)
def put_file(self, local_path, remote_path): def put_file(self, local_path, remote_path):
sftp = self._get_sftp() sftp = self._get_sftp()
sftp.put(local_path, remote_path) sftp.put(local_path, remote_path)
@ -115,13 +111,17 @@ class SSH:
if self.channel: if self.channel:
return self.channel return self.channel
counter, data = 0, '' counter = 0
self.channel = self.client.invoke_shell() self.channel = self.client.invoke_shell()
self.channel.send(b'export PS1= && stty -echo && echo Spug execute start\n') command = 'export PS1= && stty -echo'
if self.default_env:
command += f' && {self.default_env}'
command += f' && echo {self.eof} $?\n'
self.channel.send(command.encode())
while True: while True:
if self.channel.recv_ready(): if self.channel.recv_ready():
data += self.channel.recv(8196).decode() line = self.channel.recv(8196).decode()
if 'Spug execute start\r\n' in data: if self.regex.search(line):
self.stdout = self.channel.makefile('r') self.stdout = self.channel.makefile('r')
break break
elif counter >= 100: elif counter >= 100:
@ -168,6 +168,5 @@ class SSH:
return self return self
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, exc_type, exc_val, exc_tb):
print('close √')
self.client.close() self.client.close()
self.client = None self.client = None

View File

@ -3,9 +3,9 @@
* Copyright (c) <spug.dev@gmail.com> * Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License. * Released under the AGPL-3.0 License.
*/ */
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { observer, useLocalStore } from 'mobx-react'; import { observer, useLocalStore } from 'mobx-react';
import { Card, Progress, Modal, Collapse, Steps } from 'antd'; import { Card, Progress, Modal, Collapse, Steps, Skeleton } from 'antd';
import { ShrinkOutlined, CaretRightOutlined, LoadingOutlined, CloseOutlined } from '@ant-design/icons'; import { ShrinkOutlined, CaretRightOutlined, LoadingOutlined, CloseOutlined } from '@ant-design/icons';
import OutView from './OutView'; import OutView from './OutView';
import { http, X_TOKEN } from 'libs'; import { http, X_TOKEN } from 'libs';
@ -14,6 +14,8 @@ import store from './store';
function Ext1Console(props) { function Ext1Console(props) {
const outputs = useLocalStore(() => ({})); const outputs = useLocalStore(() => ({}));
const terms = useLocalStore(() => ({}));
const [fetching, setFetching] = useState(true);
useEffect(props.request.mode === 'read' ? readDeploy : doDeploy, []) useEffect(props.request.mode === 'read' ? readDeploy : doDeploy, [])
@ -22,6 +24,7 @@ function Ext1Console(props) {
http.get(`/api/deploy/request/${props.request.id}/`) http.get(`/api/deploy/request/${props.request.id}/`)
.then(res => { .then(res => {
Object.assign(outputs, res.outputs) Object.assign(outputs, res.outputs)
setTimeout(() => setFetching(false), 100)
if (res.status === '2') { if (res.status === '2') {
socket = _makeSocket() socket = _makeSocket()
} }
@ -34,6 +37,7 @@ function Ext1Console(props) {
http.post(`/api/deploy/request/${props.request.id}/`) http.post(`/api/deploy/request/${props.request.id}/`)
.then(res => { .then(res => {
Object.assign(outputs, res.outputs) Object.assign(outputs, res.outputs)
setTimeout(() => setFetching(false), 100)
socket = _makeSocket() socket = _makeSocket()
store.fetchRecords() store.fetchRecords()
}) })
@ -52,7 +56,10 @@ function Ext1Console(props) {
} else { } else {
index += 1; index += 1;
const {key, data, step, status} = JSON.parse(e.data); const {key, data, step, status} = JSON.parse(e.data);
if (data !== undefined) outputs[key].data.push(data); if (data !== undefined) {
outputs[key].data += data
if (terms[key]) terms[key].write(data)
}
if (step !== undefined) outputs[key].step = step; if (step !== undefined) outputs[key].step = step;
if (status !== undefined) outputs[key].status = status; if (status !== undefined) outputs[key].status = status;
} }
@ -73,6 +80,13 @@ function Ext1Console(props) {
store.tabModes[props.request.id] = !value store.tabModes[props.request.id] = !value
} }
function handleSetTerm(term, key) {
if (outputs[key] && outputs[key].data) {
term.write(outputs[key].data)
}
terms[key] = term
}
return store.tabModes[props.request.id] ? ( return store.tabModes[props.request.id] ? (
<Card <Card
className={styles.item} className={styles.item}
@ -103,11 +117,12 @@ function Ext1Console(props) {
<ShrinkOutlined/> <ShrinkOutlined/>
</div> </div>
]}> ]}>
<Skeleton loading={fetching} active>
<Collapse <Collapse
defaultActiveKey={'0'} defaultActiveKey="0"
className={styles.collapse} className={styles.collapse}
expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}> expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}>
{Object.values(outputs).map((item, index) => ( {Object.entries(outputs).map(([key, item], index) => (
<Collapse.Panel <Collapse.Panel
key={index} key={index}
header={ header={
@ -121,11 +136,11 @@ function Ext1Console(props) {
<StepItem title="发布后任务" item={item} step={4}/> <StepItem title="发布后任务" item={item} step={4}/>
</Steps> </Steps>
</div>}> </div>}>
<OutView records={item.data}/> <OutView setTerm={term => handleSetTerm(term, key)}/>
</Collapse.Panel> </Collapse.Panel>
))} ))}
</Collapse> </Collapse>
</Skeleton>
</Modal> </Modal>
) )
} }

View File

@ -5,7 +5,7 @@
*/ */
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { observer, useLocalStore } from 'mobx-react'; import { observer, useLocalStore } from 'mobx-react';
import { Card, Progress, Modal, Collapse, Steps } from 'antd'; import { Card, Progress, Modal, Collapse, Steps, Skeleton } from 'antd';
import { ShrinkOutlined, CaretRightOutlined, LoadingOutlined, CloseOutlined } from '@ant-design/icons'; import { ShrinkOutlined, CaretRightOutlined, LoadingOutlined, CloseOutlined } from '@ant-design/icons';
import OutView from './OutView'; import OutView from './OutView';
import { http, X_TOKEN } from 'libs'; import { http, X_TOKEN } from 'libs';
@ -13,9 +13,11 @@ import styles from './index.module.less';
import store from './store'; import store from './store';
function Ext2Console(props) { function Ext2Console(props) {
const terms = useLocalStore(() => ({}));
const outputs = useLocalStore(() => ({local: {id: 'local'}})); const outputs = useLocalStore(() => ({local: {id: 'local'}}));
const [sActions, setSActions] = useState([]); const [sActions, setSActions] = useState([]);
const [hActions, setHActions] = useState([]); const [hActions, setHActions] = useState([]);
const [fetching, setFetching] = useState(true);
useEffect(props.request.mode === 'read' ? readDeploy : doDeploy, []) useEffect(props.request.mode === 'read' ? readDeploy : doDeploy, [])
@ -25,7 +27,8 @@ function Ext2Console(props) {
.then(res => { .then(res => {
setSActions(res.s_actions); setSActions(res.s_actions);
setHActions(res.h_actions); setHActions(res.h_actions);
Object.assign(outputs, res.outputs) Object.assign(outputs, res.outputs);
setTimeout(() => setFetching(false), 100)
if (res.status === '2') { if (res.status === '2') {
socket = _makeSocket() socket = _makeSocket()
} }
@ -40,6 +43,7 @@ function Ext2Console(props) {
setSActions(res.s_actions); setSActions(res.s_actions);
setHActions(res.h_actions); setHActions(res.h_actions);
Object.assign(outputs, res.outputs) Object.assign(outputs, res.outputs)
setTimeout(() => setFetching(false), 100)
socket = _makeSocket() socket = _makeSocket()
}) })
return () => socket && socket.close() return () => socket && socket.close()
@ -57,7 +61,10 @@ function Ext2Console(props) {
} else { } else {
index += 1; index += 1;
const {key, data, step, status} = JSON.parse(e.data); const {key, data, step, status} = JSON.parse(e.data);
if (data !== undefined) outputs[key].data.push(data); if (data !== undefined) {
outputs[key].data += data
if (terms[key]) terms[key].write(data)
}
if (step !== undefined) outputs[key].step = step; if (step !== undefined) outputs[key].step = step;
if (status !== undefined) outputs[key].status = status; if (status !== undefined) outputs[key].status = status;
} }
@ -80,6 +87,13 @@ function Ext2Console(props) {
store.tabModes[props.request.id] = !value store.tabModes[props.request.id] = !value
} }
function handleSetTerm(term, key) {
if (outputs[key] && outputs[key].data) {
term.write(outputs[key].data)
}
terms[key] = term
}
const hostOutputs = Object.values(outputs).filter(x => x.id !== 'local'); const hostOutputs = Object.values(outputs).filter(x => x.id !== 'local');
return store.tabModes[props.request.id] ? ( return store.tabModes[props.request.id] ? (
<Card <Card
@ -113,7 +127,8 @@ function Ext2Console(props) {
<ShrinkOutlined/> <ShrinkOutlined/>
</div> </div>
]}> ]}>
<Collapse defaultActiveKey="0" className={styles.collapse}> <Skeleton loading={fetching} active>
<Collapse defaultActiveKey={['0']} className={styles.collapse}>
<Collapse.Panel header={( <Collapse.Panel header={(
<div className={styles.header}> <div className={styles.header}>
<b className={styles.title}/> <b className={styles.title}/>
@ -125,11 +140,12 @@ function Ext2Console(props) {
</Steps> </Steps>
</div> </div>
)}> )}>
<OutView records={outputs.local.data}/> <OutView setTerm={term => handleSetTerm(term, 'local')}/>
</Collapse.Panel> </Collapse.Panel>
</Collapse> </Collapse>
{hostOutputs.length > 0 && ( {hostOutputs.length > 0 && (
<Collapse <Collapse
accordion
defaultActiveKey="0" defaultActiveKey="0"
className={styles.collapse} className={styles.collapse}
style={{marginTop: 24}} style={{marginTop: 24}}
@ -147,11 +163,12 @@ function Ext2Console(props) {
))} ))}
</Steps> </Steps>
</div>}> </div>}>
<OutView records={item.data}/> <OutView setTerm={term => handleSetTerm(term, item.id)}/>
</Collapse.Panel> </Collapse.Panel>
))} ))}
</Collapse> </Collapse>
)} )}
</Skeleton>
</Modal> </Modal>
) )
} }

View File

@ -3,18 +3,28 @@
* Copyright (c) <spug.dev@gmail.com> * Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License. * Released under the AGPL-3.0 License.
*/ */
import React, { useRef, useEffect } from 'react'; import React, { useEffect, useRef } from 'react';
import styles from './index.module.less'; import { FitAddon } from 'xterm-addon-fit';
import { Terminal } from 'xterm';
function OutView(props) { function OutView(props) {
const el = useRef() const el = useRef()
useEffect(() => { useEffect(() => {
if (el) el.current.scrollTop = el.current.scrollHeight const fitPlugin = new FitAddon()
}) const term = new Terminal({disableStdin: true})
term.loadAddon(fitPlugin)
term.setOption('theme', {background: '#fff', foreground: '#000', selection: '#999'})
term.open(el.current)
props.setTerm(term)
fitPlugin.fit()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return ( return (
<pre ref={el} className={styles.out}>{props.records}</pre> <div style={{padding: '8px 0 0 15px'}}>
<div ref={el} style={{height: 300}}/>
</div>
) )
} }

View File

@ -92,12 +92,6 @@
color: #1890ff; color: #1890ff;
} }
} }
.out {
min-height: 40px;
max-height: 400px;
padding: 10px 15px;
}
} }
.collapse :global(.ant-collapse-content-box) { .collapse :global(.ant-collapse-content-box) {

View File

@ -18,17 +18,16 @@ function OutView(props) {
term.open(el.current) term.open(el.current)
const data = props.getOutput() const data = props.getOutput()
if (data) term.write(data) if (data) term.write(data)
term.fit = () => { term.fit = () => fitPlugin.fit()
const dimensions = fitPlugin.proposeDimensions()
if (dimensions.cols && dimensions.rows) fitPlugin.fit()
}
props.setTerm(term) props.setTerm(term)
fitPlugin.fit() fitPlugin.fit()
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
return ( return (
<div ref={el} style={{padding: '10px 15px'}}/> <div style={{padding: '8px 0 0 15px'}}>
<div ref={el}/>
</div>
) )
} }