重构开发容器创建过程

This commit is contained in:
2026-01-04 22:30:17 +08:00
parent 97313a2dc0
commit 304f7d3f82
2 changed files with 314 additions and 132 deletions

View File

@@ -112,7 +112,6 @@ export class Xterm {
private containerStatus = "";
private attachCommandSent = false;
private attachCommandSentAt?: number;
private beforeCommand?: string;
constructor(
private options: XtermOptions,
private sendCb: () => void
@@ -284,19 +283,11 @@ export class Xterm {
register(addEventListener(socket, 'message', this.onSocketData as EventListener));
register(addEventListener(socket, 'close', this.onSocketClose as EventListener));
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
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 msg = JSON.stringify({ AuthToken: this.token, columns: terminal.cols, rows: terminal.rows });
@@ -306,6 +297,12 @@ export class Xterm {
terminal.reset();
terminal.options.disableStdin = false;
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 {
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 params = new URLSearchParams({
repo: options.get('repoid') 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') +
'/devcontainer/command?' +
params
)
.then(response => response.json())
/**
* 获取并执行连接容器的命令(带重试机制)
*
* 重试机制:
* - 最多重试 5 次
* - 每次重试间隔递增1s, 2s, 3s, 4s, 5s
* - 如果成功获取命令,立即执行并停止重试
*/
@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 => {
// 验证数据有效性
if (!data || !data.command) {
throw new Error('Invalid command data received');
}
if (this.workdir === ''){
this.workdir = data.workdir;
}
if (data.status !== '4' && data.status !== '0') {
if(this.containerStatus !== data.status){
this.sendData(data.command);
}
this.containerStatus = data.status;
} else {
if (this.containerStatus !== '4'){
this.writeData("\x1b[31mCreation completed.\x1b[0m\r\n");
}
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;
}
// 执行连接容器的命令(只执行一次)
const parts = data.command.split('\n');
if (parts[0] && !this.connectStatus) {
console.log('[Xterm] Successfully loaded connection command, executing...');
this.sendData(parts[0]+"\n");
this.attachCommandSent = true;
this.attachCommandSentAt = Date.now();
}
this.postAttachCommand = parts;
})
.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
public changeContainerStatus(v: string){
this.containerStatus = v;
}
@bind
private parseOptsFromUrlQuery(query: string): Preferences {
const { terminal } = this;
@@ -439,11 +468,7 @@ export class Xterm {
const decodedData = textDecoder.decode(data);
console.log('[ttyd] output:', decodedData);
const compactOutput = decodedData.replace(/\s/g, '');
const options = new URLSearchParams(decodeURIComponent(window.location.search));
const params = new URLSearchParams({
repo: options.get('repoid') as string,
user: options.get('userid') as string,
});
const { options } = this.getUrlParams();
if (options.get('type') === 'docker') {
// 保存host的标题
if (this.hostTitle === ''){
@@ -457,8 +482,8 @@ export class Xterm {
this.attachCommandSentAt = undefined;
this.postAttachCommandStatus = false;
}
// this.connectStatus = true 连接完成
//由于第二条docker命令中包含Successfully connected to the devcontainer,需要过滤否则会导致轮询终止卡在状态2
// 检测连接完成:监听 "Successfully connected to the devcontainer" 消息
// 这条消息是由连接命令中的 echo "$WEB_TERMINAL_HELLO" 输出的
if (!this.connectStatus) {
const sanitizedOutput = this.stripAnsi(decodedData).replace(/\r/g, '\n');
const combinedOutput = this.connectionMessageBuffer + sanitizedOutput;
@@ -469,12 +494,12 @@ export class Xterm {
this.connectStatus = true;
this.connectionMessageBuffer = '';
this.attachCommandSentAt = undefined;
if (this.intervalID) {
clearInterval(this.intervalID);
}
console.log('[Xterm] Connection established, enabling terminal input');
// 确保终端输入已启用
this.terminal.options.disableStdin = false;
}
}
// 连接完成之前,不输出标题和docker命令
// 连接完成之前,过滤掉 docker exec 命令的标题输出ANSI 码和 docker-H 开头的输出)
if (
!(this.connectStatus === false &&
(textDecoder.decode(data).includes('\x1b') ||
@@ -484,6 +509,7 @@ export class Xterm {
}
// 连接完成且出现容器的标题且没有执行过postAttach命令
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++){
this.sendData(this.postAttachCommand[i]+'\n');
}
@@ -495,12 +521,10 @@ export class Xterm {
}
break;
case Command.SET_WINDOW_TITLE:
console.log('SET_WINDOW_TITLESET_WINDOW_TITLE');
this.title = textDecoder.decode(data);
document.title = this.title;
break;
case Command.SET_PREFERENCES:
console.log('SET_PREFERENCESSET_PREFERENCESSET_PREFERENCES');
this.applyPreferences({
...this.options.clientOptions,
...JSON.parse(textDecoder.decode(data)),