Merge pull request '重构开发容器创建过程' (#3) from fix/issue-32 into main
Some checks failed
docker / build (push) Failing after 2m44s
Some checks failed
docker / build (push) Failing after 2m44s
Reviewed-on: #3
This commit is contained in:
@@ -16,22 +16,15 @@ interface State {
|
|||||||
export class Terminal extends Component<Props, State> {
|
export class Terminal extends Component<Props, State> {
|
||||||
private container: HTMLElement;
|
private container: HTMLElement;
|
||||||
private xterm: Xterm;
|
private xterm: Xterm;
|
||||||
private intervalID: NodeJS.Timeout;
|
private pollingIntervalID: NodeJS.Timeout | null = null;
|
||||||
private currentDevcontainer = {
|
private currentDevcontainer = {
|
||||||
title: 'Devcontainer Info',
|
state: '-1', // 容器状态 (0-5, -1)
|
||||||
detail: 'No Devcontainer Created yet',
|
steps: [] as any[],
|
||||||
port: '',
|
|
||||||
ip:'',
|
|
||||||
steps: [
|
|
||||||
// {
|
|
||||||
// summary: '',
|
|
||||||
// duration: '',
|
|
||||||
// status: '',
|
|
||||||
// logs:{
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
private isDockerType = false;
|
||||||
|
private apiBaseUrl = '';
|
||||||
|
private apiParams = new URLSearchParams();
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super();
|
super();
|
||||||
this.xterm = new Xterm(props, this.showModal);
|
this.xterm = new Xterm(props, this.showModal);
|
||||||
@@ -41,44 +34,19 @@ export class Terminal extends Component<Props, State> {
|
|||||||
await this.xterm.refreshToken();
|
await this.xterm.refreshToken();
|
||||||
const options = new URLSearchParams(decodeURIComponent(window.location.search));
|
const options = new URLSearchParams(decodeURIComponent(window.location.search));
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
this.isDockerType = options.get('type') === 'docker';
|
||||||
|
this.apiBaseUrl = `http://${options.get('domain')}:${options.get('port')}/${options.get('user')}/${options.get('repo')}`;
|
||||||
|
this.apiParams = new URLSearchParams({
|
||||||
repo: options.get('repoid') as string,
|
repo: options.get('repoid') as string,
|
||||||
user: options.get('userid') as string,
|
user: options.get('userid') as string,
|
||||||
});
|
});
|
||||||
fetch('http://' +
|
|
||||||
options.get('domain') +
|
|
||||||
':'+
|
|
||||||
options.get('port') +
|
|
||||||
'/' +
|
|
||||||
options.get('user') +
|
|
||||||
'/' +
|
|
||||||
options.get('repo') +
|
|
||||||
'/devcontainer/status?' +
|
|
||||||
params
|
|
||||||
)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.status !== '-1') {
|
|
||||||
if (options.get('type') === 'docker') {
|
|
||||||
this.xterm.open(this.container);
|
|
||||||
this.xterm.changeContainerStatus(data.status);
|
|
||||||
this.xterm.connect();
|
|
||||||
} else {
|
|
||||||
this.intervalID = setInterval(this.loadOutput, 8000);
|
|
||||||
this.xterm.open(this.container);
|
|
||||||
this.xterm.changeUrl(this.currentDevcontainer.ip, this.currentDevcontainer.port)
|
|
||||||
this.xterm.changeStatus(true);
|
|
||||||
this.xterm.connect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// 首次检查状态,根据状态启动相应的轮询策略
|
||||||
|
await this.checkStatusAndStartPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
this.stopPolling();
|
||||||
this.xterm.dispose();
|
this.xterm.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,47 +75,237 @@ export class Terminal extends Component<Props, State> {
|
|||||||
if (files) this.xterm.sendFile(files);
|
if (files) this.xterm.sendFile(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首次状态检查并启动相应的轮询策略
|
||||||
|
*
|
||||||
|
* 流程:
|
||||||
|
* 1. 状态 0-4: 启动日志轮询,显示容器创建日志
|
||||||
|
* - 日志轮询会在状态变为 5 时自动停止并获取连接命令
|
||||||
|
* - 或者检测到 DEVCONTAINER_STARTUP_COMPLETE 后主动检查状态并获取连接命令
|
||||||
|
* 2. 状态 5: 直接获取并执行连接命令(只执行一次,不需要轮询)
|
||||||
|
*/
|
||||||
@bind
|
@bind
|
||||||
private loadOutput() {
|
private async checkStatusAndStartPolling() {
|
||||||
const options = new URLSearchParams(decodeURIComponent(window.location.search));
|
try {
|
||||||
const params = new URLSearchParams({
|
const response = await fetch(`${this.apiBaseUrl}/devcontainer/status?${this.apiParams}`);
|
||||||
repo: options.get('repoid') as string,
|
const data = await response.json();
|
||||||
user: options.get('userid') as string,
|
|
||||||
});
|
if (data.status === '-1') {
|
||||||
|
// 容器不存在,不启动轮询
|
||||||
fetch(
|
return;
|
||||||
'http://' + options.get('domain') + ':'+ options.get('port') +'/' +
|
}
|
||||||
options.get('user') +
|
|
||||||
'/' +
|
// 打开终端并连接 WebSocket
|
||||||
options.get('repo') +
|
this.xterm.open(this.container);
|
||||||
'/devcontainer/output?' +
|
if (this.isDockerType) {
|
||||||
params
|
this.xterm.changeContainerStatus(data.status);
|
||||||
)
|
}
|
||||||
.then(response => response.json())
|
this.xterm.connect();
|
||||||
.then(job => {
|
|
||||||
if (!job) {
|
const status = parseInt(data.status);
|
||||||
clearInterval(this.intervalID);
|
|
||||||
this.intervalID = null as any;
|
if (this.isDockerType) {
|
||||||
return;
|
// Docker 类型:根据状态选择轮询策略
|
||||||
|
if (status >= 0 && status < 5) {
|
||||||
|
// 状态 0-4: 创建中,启动日志轮询(3秒一次)
|
||||||
|
// 日志轮询会在状态变为 5 时自动停止并获取连接命令
|
||||||
|
this.startLogPolling(3000);
|
||||||
|
} else if (status === 5) {
|
||||||
|
// 状态 5: 创建完成,直接获取并执行连接命令
|
||||||
|
this.loadCommandOnce();
|
||||||
}
|
}
|
||||||
if(this.currentDevcontainer.steps.length < job.currentDevcontainer.steps.length){
|
} else {
|
||||||
for(let i = this.currentDevcontainer.steps.length; i < job.currentDevcontainer.steps.length; i++) {
|
// 非 Docker 类型:直接启动日志轮询
|
||||||
this.xterm.writeData(job.currentDevcontainer.steps[i].summary);
|
this.startLogPolling(8000);
|
||||||
this.xterm.writeData('\r\n');
|
}
|
||||||
for(let j = 0; j < job.currentDevcontainer.steps[i].logs.length; j++) {
|
} catch (error) {
|
||||||
this.xterm.writeData(job.currentDevcontainer.steps[i].logs[j].message.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n'));
|
console.error('Error checking status:', error);
|
||||||
this.xterm.writeData('\r\n');
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动日志轮询(用于显示容器创建日志)
|
||||||
|
*/
|
||||||
|
@bind
|
||||||
|
private startLogPolling(interval: number = 1000) {
|
||||||
|
this.stopPolling();
|
||||||
|
// 立即加载一次日志
|
||||||
|
this.loadLogs();
|
||||||
|
// 然后定期轮询
|
||||||
|
this.pollingIntervalID = setInterval(this.loadLogs, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取并执行连接命令(只执行一次,不需要轮询)
|
||||||
|
*/
|
||||||
|
@bind
|
||||||
|
private loadCommandOnce() {
|
||||||
|
this.stopPolling();
|
||||||
|
this.xterm.loadCommandOnce();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止所有轮询
|
||||||
|
*/
|
||||||
|
@bind
|
||||||
|
private stopPolling() {
|
||||||
|
if (this.pollingIntervalID) {
|
||||||
|
clearInterval(this.pollingIntervalID);
|
||||||
|
this.pollingIntervalID = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化日志消息的换行符
|
||||||
|
*/
|
||||||
|
private normalizeLogMessage(message: string): string {
|
||||||
|
return message.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输出日志行
|
||||||
|
*/
|
||||||
|
private writeLogLine(message: string) {
|
||||||
|
this.xterm.writeData(this.normalizeLogMessage(message));
|
||||||
|
this.xterm.writeData('\r\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理步骤日志更新
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
private processStepLogs(newSteps: any[], oldSteps: any[]): boolean {
|
||||||
|
let detectedStartupComplete = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < newSteps.length; i++) {
|
||||||
|
const newStep = newSteps[i];
|
||||||
|
const oldStep = oldSteps[i] as any;
|
||||||
|
|
||||||
|
// 如果是新步骤,输出步骤标题
|
||||||
|
if (!oldStep) {
|
||||||
|
// 如果不是第一个步骤,在前面添加换行
|
||||||
|
if (i > 0) {
|
||||||
|
this.xterm.writeData('\r\n');
|
||||||
|
}
|
||||||
|
this.xterm.writeData(`${newStep.summary}\r\n`);
|
||||||
|
// 输出该步骤的所有日志
|
||||||
|
if (newStep.logs && newStep.logs.length > 0) {
|
||||||
|
for (let j = 0; j < newStep.logs.length; j++) {
|
||||||
|
this.writeLogLine(newStep.logs[j].message);
|
||||||
|
// 检测 DEVCONTAINER_STARTUP_COMPLETE
|
||||||
|
if (newStep.logs[j].message.includes('DEVCONTAINER_STARTUP_COMPLETE')) {
|
||||||
|
detectedStartupComplete = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 输出新增的日志行
|
||||||
|
if (newStep.logs && oldStep.logs && newStep.logs.length > oldStep.logs.length) {
|
||||||
|
for (let j = oldStep.logs.length; j < newStep.logs.length; j++) {
|
||||||
|
this.writeLogLine(newStep.logs[j].message);
|
||||||
|
// 检测 DEVCONTAINER_STARTUP_COMPLETE
|
||||||
|
if (newStep.logs[j].message.includes('DEVCONTAINER_STARTUP_COMPLETE')) {
|
||||||
|
detectedStartupComplete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return detectedStartupComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查并处理状态变化
|
||||||
|
*
|
||||||
|
* 日志轮询结束条件:
|
||||||
|
* 1. 状态从 0-4 变为 5:自动停止日志轮询,获取并执行连接命令
|
||||||
|
*
|
||||||
|
* @param newState 新状态(从日志接口返回的 job.currentDevcontainer.state 获取)
|
||||||
|
* @returns 是否已获取连接命令
|
||||||
|
*/
|
||||||
|
private checkAndHandleStatusChange(newState: string): boolean {
|
||||||
|
const prevStatus = parseInt(this.currentDevcontainer.state);
|
||||||
|
const currentStatus = parseInt(newState);
|
||||||
|
|
||||||
|
// 状态变化处理:从创建中(0-4)变为完成(5)时,停止日志轮询并获取连接命令
|
||||||
|
// 状态是从日志接口 (/devcontainer/logs) 返回的数据中获取的
|
||||||
|
if (this.isDockerType && prevStatus >= 0 && prevStatus < 5 && currentStatus === 5) {
|
||||||
|
console.log('[Terminal] Container creation completed (status 0-4 -> 5), stopping log polling and loading command');
|
||||||
|
this.stopPolling();
|
||||||
|
this.loadCommandOnce();
|
||||||
|
return true; // 已切换
|
||||||
|
}
|
||||||
|
return false; // 未切换
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主动检查状态(用于检测到 DEVCONTAINER_STARTUP_COMPLETE 后)
|
||||||
|
*
|
||||||
|
* 当检测到 DEVCONTAINER_STARTUP_COMPLETE 但日志接口返回的状态还不是 5 时,
|
||||||
|
* 主动查询状态接口,如果状态为 5,则停止日志轮询并切换到命令轮询
|
||||||
|
*/
|
||||||
|
@bind
|
||||||
|
private async checkStatusOnce() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiBaseUrl}/devcontainer/status?${this.apiParams}`);
|
||||||
|
const data = await response.json();
|
||||||
|
const status = parseInt(data.status);
|
||||||
|
|
||||||
|
if (status === 5) {
|
||||||
|
console.log('[Terminal] Status check: container is ready (status 5), stopping log polling and loading command');
|
||||||
|
this.stopPolling();
|
||||||
|
this.loadCommandOnce();
|
||||||
|
} else {
|
||||||
|
console.log('[Terminal] Status check: container not ready yet (status', status, '), log polling will continue');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载容器创建日志
|
||||||
|
*/
|
||||||
|
@bind
|
||||||
|
private loadLogs() {
|
||||||
|
// 使用日志接口(从内存中获取实时日志)
|
||||||
|
// 日志接口返回的数据包含状态信息:job.currentDevcontainer.state
|
||||||
|
fetch(`${this.apiBaseUrl}/devcontainer/logs?${this.apiParams}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(job => {
|
||||||
|
if (!job || !job.currentDevcontainer) {
|
||||||
|
// 如果日志接口没有数据,停止轮询
|
||||||
|
this.stopPolling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSteps = job.currentDevcontainer.steps;
|
||||||
|
const oldSteps = this.currentDevcontainer.steps;
|
||||||
|
|
||||||
|
// 处理步骤日志更新,检测是否出现 DEVCONTAINER_STARTUP_COMPLETE
|
||||||
|
const detectedStartupComplete = this.processStepLogs(newSteps, oldSteps);
|
||||||
|
|
||||||
|
// 更新当前状态(从日志接口返回的状态)
|
||||||
this.currentDevcontainer = job.currentDevcontainer;
|
this.currentDevcontainer = job.currentDevcontainer;
|
||||||
if (this.currentDevcontainer.detail === '4' && this.intervalID) {
|
|
||||||
clearInterval(this.intervalID);
|
// 检查并处理状态变化:从日志接口返回的状态中检测,如果状态变为 5,停止日志轮询并切换到命令轮询
|
||||||
this.intervalID = null as any;
|
const statusChanged = this.checkAndHandleStatusChange(this.currentDevcontainer.state);
|
||||||
|
|
||||||
|
// 如果检测到 DEVCONTAINER_STARTUP_COMPLETE 但状态还没变为 5,主动检查一次状态
|
||||||
|
// 因为状态更新可能有延迟,需要主动轮询状态接口
|
||||||
|
if (detectedStartupComplete && !statusChanged && this.isDockerType) {
|
||||||
|
console.log('[Terminal] Detected DEVCONTAINER_STARTUP_COMPLETE but status not 5 yet, checking status...');
|
||||||
|
// 延迟一点再检查,给后端一点时间更新状态
|
||||||
|
setTimeout(() => {
|
||||||
|
this.checkStatusOnce();
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error:', error);
|
console.error('Error loading logs:', error);
|
||||||
|
// 如果日志接口失败,停止轮询
|
||||||
|
this.stopPolling();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ export class Xterm {
|
|||||||
private containerStatus = "";
|
private containerStatus = "";
|
||||||
private attachCommandSent = false;
|
private attachCommandSent = false;
|
||||||
private attachCommandSentAt?: number;
|
private attachCommandSentAt?: number;
|
||||||
private beforeCommand?: string;
|
|
||||||
constructor(
|
constructor(
|
||||||
private options: XtermOptions,
|
private options: XtermOptions,
|
||||||
private sendCb: () => void
|
private sendCb: () => void
|
||||||
@@ -284,19 +283,11 @@ export class Xterm {
|
|||||||
register(addEventListener(socket, 'message', this.onSocketData as EventListener));
|
register(addEventListener(socket, 'message', this.onSocketData as EventListener));
|
||||||
register(addEventListener(socket, 'close', this.onSocketClose as EventListener));
|
register(addEventListener(socket, 'close', this.onSocketClose as EventListener));
|
||||||
register(addEventListener(socket, 'error', () => (this.doReconnect = false)));
|
register(addEventListener(socket, 'error', () => (this.doReconnect = false)));
|
||||||
const options = new URLSearchParams(decodeURIComponent(window.location.search));
|
|
||||||
if (options.get('type') === 'docker') {
|
|
||||||
if(this.containerStatus === '4' || this.containerStatus === '-1'){
|
|
||||||
this.intervalID = setInterval(this.loadCommand, 1000);
|
|
||||||
}else{
|
|
||||||
this.intervalID = setInterval(this.loadCommand, 8000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
private onSocketOpen() {
|
private onSocketOpen() {
|
||||||
console.log('[ttyd] websocket connection opened');
|
console.log('[webTerminal] WebSocket opened, containerStatus:', this.containerStatus, 'connectStatus:', this.connectStatus, 'attachCommandSent:', this.attachCommandSent);
|
||||||
|
|
||||||
const { textEncoder, terminal, overlayAddon } = this;
|
const { textEncoder, terminal, overlayAddon } = this;
|
||||||
const msg = JSON.stringify({ AuthToken: this.token, columns: terminal.cols, rows: terminal.rows });
|
const msg = JSON.stringify({ AuthToken: this.token, columns: terminal.cols, rows: terminal.rows });
|
||||||
@@ -306,6 +297,12 @@ export class Xterm {
|
|||||||
terminal.reset();
|
terminal.reset();
|
||||||
terminal.options.disableStdin = false;
|
terminal.options.disableStdin = false;
|
||||||
overlayAddon.showOverlay('Reconnected', 300);
|
overlayAddon.showOverlay('Reconnected', 300);
|
||||||
|
// 重新连接后,如果状态是5且未连接,重置连接状态以便重新发送连接命令
|
||||||
|
if (this.containerStatus === '5' && !this.connectStatus) {
|
||||||
|
console.log('[webTerminal] Reconnected, resetting attach command state');
|
||||||
|
this.attachCommandSent = false;
|
||||||
|
this.attachCommandSentAt = undefined;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.opened = true;
|
this.opened = true;
|
||||||
}
|
}
|
||||||
@@ -343,57 +340,89 @@ export class Xterm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
/**
|
||||||
private loadCommand() {
|
* 获取 URL 查询参数
|
||||||
|
*/
|
||||||
|
private getUrlParams(): { options: URLSearchParams; params: URLSearchParams; baseUrl: string } {
|
||||||
const options = new URLSearchParams(decodeURIComponent(window.location.search));
|
const options = new URLSearchParams(decodeURIComponent(window.location.search));
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
repo: options.get('repoid') as string,
|
repo: options.get('repoid') as string,
|
||||||
user: options.get('userid') as string,
|
user: options.get('userid') as string,
|
||||||
});
|
});
|
||||||
|
const baseUrl = `http://${options.get('domain')}:${options.get('port')}/${options.get('user')}/${options.get('repo')}`;
|
||||||
|
return { options, params, baseUrl };
|
||||||
|
}
|
||||||
|
|
||||||
fetch(
|
/**
|
||||||
'http://' + options.get('domain') + ':'+ options.get('port') +'/' +
|
* 获取并执行连接容器的命令(带重试机制)
|
||||||
options.get('user') +
|
*
|
||||||
'/' +
|
* 重试机制:
|
||||||
options.get('repo') +
|
* - 最多重试 5 次
|
||||||
'/devcontainer/command?' +
|
* - 每次重试间隔递增(1s, 2s, 3s, 4s, 5s)
|
||||||
params
|
* - 如果成功获取命令,立即执行并停止重试
|
||||||
)
|
*/
|
||||||
.then(response => response.json())
|
@bind
|
||||||
|
public loadCommandOnce() {
|
||||||
|
this.loadCommandWithRetry(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带重试的命令获取
|
||||||
|
* @param retryCount 当前重试次数
|
||||||
|
*/
|
||||||
|
@bind
|
||||||
|
private loadCommandWithRetry(retryCount: number = 0) {
|
||||||
|
const maxRetries = 5;
|
||||||
|
const { params, baseUrl } = this.getUrlParams();
|
||||||
|
|
||||||
|
fetch(`${baseUrl}/devcontainer/command?${params}`)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
// 验证数据有效性
|
||||||
|
if (!data || !data.command) {
|
||||||
|
throw new Error('Invalid command data received');
|
||||||
|
}
|
||||||
|
|
||||||
if (this.workdir === ''){
|
if (this.workdir === ''){
|
||||||
this.workdir = data.workdir;
|
this.workdir = data.workdir;
|
||||||
}
|
}
|
||||||
if (data.status !== '4' && data.status !== '0') {
|
|
||||||
if(this.containerStatus !== data.status){
|
// 执行连接容器的命令(只执行一次)
|
||||||
this.sendData(data.command);
|
const parts = data.command.split('\n');
|
||||||
}
|
if (parts[0] && !this.connectStatus) {
|
||||||
this.containerStatus = data.status;
|
console.log('[Xterm] Successfully loaded connection command, executing...');
|
||||||
} else {
|
this.sendData(parts[0]+"\n");
|
||||||
if (this.containerStatus !== '4'){
|
this.attachCommandSent = true;
|
||||||
this.writeData("\x1b[31mCreation completed.\x1b[0m\r\n");
|
this.attachCommandSentAt = Date.now();
|
||||||
}
|
|
||||||
this.containerStatus = data.status;
|
|
||||||
if (data.status === '4') {
|
|
||||||
const parts = data.command.split('\n');
|
|
||||||
const shouldResend = this.attachCommandSent && this.attachCommandSentAt !== undefined && Date.now() - this.attachCommandSentAt > 5000;
|
|
||||||
if ((!this.attachCommandSent || shouldResend) && !this.connectStatus && parts[0]) {
|
|
||||||
this.sendData(parts[0]+"\n");
|
|
||||||
this.attachCommandSent = true;
|
|
||||||
this.attachCommandSentAt = Date.now();
|
|
||||||
}
|
|
||||||
this.postAttachCommand = parts;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this.postAttachCommand = parts;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error:', error);
|
console.error(`[Xterm] Error loading command (attempt ${retryCount + 1}/${maxRetries}):`, error);
|
||||||
|
|
||||||
|
// 如果还有重试次数,继续重试
|
||||||
|
if (retryCount < maxRetries - 1) {
|
||||||
|
const delay = (retryCount + 1) * 1000; // 递增延迟:1s, 2s, 3s, 4s, 5s
|
||||||
|
console.log(`[Xterm] Retrying command load in ${delay}ms...`);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loadCommandWithRetry(retryCount + 1);
|
||||||
|
}, delay);
|
||||||
|
} else {
|
||||||
|
console.error('[Xterm] Failed to load command after all retries');
|
||||||
|
// 可以在这里显示错误提示给用户
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@bind
|
@bind
|
||||||
public changeContainerStatus(v: string){
|
public changeContainerStatus(v: string){
|
||||||
this.containerStatus = v;
|
this.containerStatus = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
private parseOptsFromUrlQuery(query: string): Preferences {
|
private parseOptsFromUrlQuery(query: string): Preferences {
|
||||||
const { terminal } = this;
|
const { terminal } = this;
|
||||||
@@ -439,11 +468,7 @@ export class Xterm {
|
|||||||
const decodedData = textDecoder.decode(data);
|
const decodedData = textDecoder.decode(data);
|
||||||
console.log('[ttyd] output:', decodedData);
|
console.log('[ttyd] output:', decodedData);
|
||||||
const compactOutput = decodedData.replace(/\s/g, '');
|
const compactOutput = decodedData.replace(/\s/g, '');
|
||||||
const options = new URLSearchParams(decodeURIComponent(window.location.search));
|
const { options } = this.getUrlParams();
|
||||||
const params = new URLSearchParams({
|
|
||||||
repo: options.get('repoid') as string,
|
|
||||||
user: options.get('userid') as string,
|
|
||||||
});
|
|
||||||
if (options.get('type') === 'docker') {
|
if (options.get('type') === 'docker') {
|
||||||
// 保存host的标题
|
// 保存host的标题
|
||||||
if (this.hostTitle === ''){
|
if (this.hostTitle === ''){
|
||||||
@@ -457,8 +482,8 @@ export class Xterm {
|
|||||||
this.attachCommandSentAt = undefined;
|
this.attachCommandSentAt = undefined;
|
||||||
this.postAttachCommandStatus = false;
|
this.postAttachCommandStatus = false;
|
||||||
}
|
}
|
||||||
// this.connectStatus = true 连接完成
|
// 检测连接完成:监听 "Successfully connected to the devcontainer" 消息
|
||||||
//由于第二条docker命令中包含Successfully connected to the devcontainer,需要过滤否则会导致轮询终止,卡在状态2
|
// 这条消息是由连接命令中的 echo "$WEB_TERMINAL_HELLO" 输出的
|
||||||
if (!this.connectStatus) {
|
if (!this.connectStatus) {
|
||||||
const sanitizedOutput = this.stripAnsi(decodedData).replace(/\r/g, '\n');
|
const sanitizedOutput = this.stripAnsi(decodedData).replace(/\r/g, '\n');
|
||||||
const combinedOutput = this.connectionMessageBuffer + sanitizedOutput;
|
const combinedOutput = this.connectionMessageBuffer + sanitizedOutput;
|
||||||
@@ -469,12 +494,12 @@ export class Xterm {
|
|||||||
this.connectStatus = true;
|
this.connectStatus = true;
|
||||||
this.connectionMessageBuffer = '';
|
this.connectionMessageBuffer = '';
|
||||||
this.attachCommandSentAt = undefined;
|
this.attachCommandSentAt = undefined;
|
||||||
if (this.intervalID) {
|
console.log('[Xterm] Connection established, enabling terminal input');
|
||||||
clearInterval(this.intervalID);
|
// 确保终端输入已启用
|
||||||
}
|
this.terminal.options.disableStdin = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 连接完成之前,不输出标题和docker命令
|
// 连接完成之前,过滤掉 docker exec 命令的标题输出(ANSI 码和 docker-H 开头的输出)
|
||||||
if (
|
if (
|
||||||
!(this.connectStatus === false &&
|
!(this.connectStatus === false &&
|
||||||
(textDecoder.decode(data).includes('\x1b') ||
|
(textDecoder.decode(data).includes('\x1b') ||
|
||||||
@@ -484,6 +509,7 @@ export class Xterm {
|
|||||||
}
|
}
|
||||||
// 连接完成且出现容器的标题,且没有执行过postAttach命令
|
// 连接完成且出现容器的标题,且没有执行过postAttach命令
|
||||||
if (this.connectStatus && compactOutput.includes(this.workdir) && !this.postAttachCommandStatus){
|
if (this.connectStatus && compactOutput.includes(this.workdir) && !this.postAttachCommandStatus){
|
||||||
|
console.log('[Xterm] Detected workdir in output, executing postAttachCommand');
|
||||||
for (let i = 1; i < this.postAttachCommand.length; i++){
|
for (let i = 1; i < this.postAttachCommand.length; i++){
|
||||||
this.sendData(this.postAttachCommand[i]+'\n');
|
this.sendData(this.postAttachCommand[i]+'\n');
|
||||||
}
|
}
|
||||||
@@ -495,12 +521,10 @@ export class Xterm {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Command.SET_WINDOW_TITLE:
|
case Command.SET_WINDOW_TITLE:
|
||||||
console.log('SET_WINDOW_TITLESET_WINDOW_TITLE');
|
|
||||||
this.title = textDecoder.decode(data);
|
this.title = textDecoder.decode(data);
|
||||||
document.title = this.title;
|
document.title = this.title;
|
||||||
break;
|
break;
|
||||||
case Command.SET_PREFERENCES:
|
case Command.SET_PREFERENCES:
|
||||||
console.log('SET_PREFERENCESSET_PREFERENCESSET_PREFERENCES');
|
|
||||||
this.applyPreferences({
|
this.applyPreferences({
|
||||||
...this.options.clientOptions,
|
...this.options.clientOptions,
|
||||||
...JSON.parse(textDecoder.decode(data)),
|
...JSON.parse(textDecoder.decode(data)),
|
||||||
|
|||||||
Reference in New Issue
Block a user