Skip to content

Commit bc7bfc5

Browse files
authored
Merge pull request #115 from Coding/hackape/tty-reconnection
Terminal 断线重连问题
2 parents 568bdc9 + 3b2d273 commit bc7bfc5

File tree

12 files changed

+264
-153
lines changed

12 files changed

+264
-153
lines changed

app/backendAPI/websocketClient.js

Lines changed: 0 additions & 52 deletions
This file was deleted.

app/backendAPI/websocketClients.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { Stomp } from 'stompjs/lib/stomp'
2+
import SockJS from 'sockjs-client'
3+
import getBackoff from 'utils/getBackoff'
4+
import config from 'config'
5+
import { autorun, runInAction } from 'mobx'
6+
7+
const log = console.log || (x => x)
8+
const warn = console.warn || (x => x)
9+
10+
const io = require(__RUN_MODE__ ? 'socket.io-client/dist/socket.io.min.js' : 'socket.io-client-legacy/dist/socket.io.min.js')
11+
12+
class FsSocketClient {
13+
constructor () {
14+
if (FsSocketClient.$$singleton) return FsSocketClient.$$singleton
15+
const url = config.isPlatform ?
16+
`${config.wsURL}/sockjs/${config.spaceKey}`
17+
: `${config.baseURL}/sockjs/`
18+
// SockJS auto connects at initiation
19+
this.sockJSConfigs = [url, {}, { server: `${config.spaceKey}`, transports: 'websocket' }]
20+
this.backoff = getBackoff({
21+
delayMin: 50,
22+
delayMax: 5000,
23+
})
24+
this.maxAttempts = 7
25+
FsSocketClient.$$singleton = this
26+
}
27+
28+
connect (connectCallback, errorCallback) {
29+
const self = this
30+
if (!this.socket || !this.stompClient) {
31+
this.socket = new SockJS(...this.sockJSConfigs)
32+
this.stompClient = Stomp.over(this.socket)
33+
this.stompClient.debug = false // stop logging PING/PONG
34+
}
35+
self.stompClient.connect({}, function success () {
36+
runInAction(() => config.fsSocketConnected = true)
37+
self.backoff.reset()
38+
connectCallback.call(this)
39+
}, function error () {
40+
log('fsSocket error')
41+
switch (self.socket.readyState) {
42+
case SockJS.CLOSING:
43+
case SockJS.CLOSED:
44+
runInAction(() => config.fsSocketConnected = false)
45+
self.reconnect(connectCallback, errorCallback)
46+
break
47+
case SockJS.OPEN:
48+
log('FRAME ERROR', arguments[0])
49+
break
50+
default:
51+
}
52+
errorCallback(arguments)
53+
})
54+
}
55+
56+
reconnect (connectCallback, errorCallback) {
57+
if (config.fsSocketConnected) return
58+
log(`try reconnect fsSocket ${this.backoff.attempts}`)
59+
// unset this.socket
60+
this.socket = undefined
61+
if (this.backoff.attempts <= this.maxAttempts) {
62+
const retryDelay = this.backoff.duration()
63+
log(`Retry after ${retryDelay}ms`)
64+
const timer = setTimeout(
65+
this.connect.bind(this, connectCallback, errorCallback)
66+
, retryDelay)
67+
} else {
68+
this.backoff.reset()
69+
warn('Sock connected failed, something may be broken, reload page and try again')
70+
}
71+
}
72+
}
73+
74+
75+
class TtySocketClient {
76+
constructor () {
77+
if (TtySocketClient.$$singleton) return TtySocketClient.$$singleton
78+
if (config.isPlatform) {
79+
const wsUrl = config.wsURL
80+
const firstSlashIdx = wsUrl.indexOf('/', 8)
81+
let [host, path] = firstSlashIdx === -1 ? [wsUrl, ''] : [wsUrl.substring(0, firstSlashIdx), wsUrl.substring(firstSlashIdx)]
82+
this.socket = io.connect(host, {
83+
forceNew: true,
84+
reconnection: false,
85+
autoConnect: false, // <- will manually handle all connect/reconnect behavior
86+
reconnectionDelay: 1500,
87+
reconnectionDelayMax: 10000,
88+
reconnectionAttempts: 5,
89+
path: `${path}/tty/${config.shardingGroup}/${config.spaceKey}/connect`,
90+
transports: ['websocket']
91+
})
92+
} else {
93+
this.socket = io.connect(config.baseURL, { 'resource': 'coding-ide-tty1' })
94+
}
95+
96+
this.backoff = getBackoff({
97+
delayMin: 1500,
98+
delayMax: 10000,
99+
})
100+
this.maxAttempts = 5
101+
102+
TtySocketClient.$$singleton = this
103+
return this
104+
}
105+
106+
// manually handle all connect/reconnect behavior
107+
connectingPromise = undefined
108+
connect () {
109+
// Need to make sure EVERY ATTEMPT to connect has ensured `fsSocketConnected == true`
110+
if (this.socket.connected || this.connectingPromise) return this.connectingPromise
111+
let resolve, reject
112+
this.connectingPromise = new Promise((rsv, rjt) => { resolve = rsv; reject = rjt })
113+
const dispose = autorun(() => { if (config.fsSocketConnected) resolve(true) })
114+
this.connectingPromise.then(() => {
115+
dispose()
116+
this.connectingPromise = undefined
117+
118+
// below is the actual working part of `connect()` method,
119+
// all logic above is just for ensuring `fsSocketConnected == true`
120+
this.socket.io.connect(err => {
121+
if (err) {
122+
runInAction(() => config.ttySocketConnected = false)
123+
return this.reconnect()
124+
}
125+
// success!
126+
runInAction(() => config.ttySocketConnected = true)
127+
this.backoff.reset()
128+
})
129+
this.socket.connect()
130+
})
131+
}
132+
133+
reconnect () {
134+
log(`try reconnect ttySocket ${this.backoff.attempts}`)
135+
if (this.backoff.attempts <= this.maxAttempts && !this.socket.connected) {
136+
const timer = setTimeout(() => {
137+
this.connect()
138+
}, this.backoff.duration())
139+
} else {
140+
warn(`TTY reconnection fail after ${this.backoff.attempts} attempts`)
141+
this.backoff.reset()
142+
}
143+
}
144+
145+
}
146+
147+
export { FsSocketClient, TtySocketClient }

app/backendAPI/workspaceAPI.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/* @flow weak */
22
import config from '../config'
33
import { request } from '../utils'
4-
import Client from './websocketClient'
4+
import { FsSocketClient } from './websocketClients'
55

66
var connectedResolve
7-
export const websocketConnectedPromise = new Promise((rs, rj) => connectedResolve = rs)
7+
export const fsSocketConnectedPromise = new Promise((rs, rj) => connectedResolve = rs)
88

99
export function isWorkspaceExist () {
1010
return request.get(`/workspaces/${config.spaceKey}`).catch(() => false).then(() => true)
@@ -22,9 +22,11 @@ export function createWorkspace (options) {
2222

2323
export function connectWebsocketClient () {
2424
return new Promise(function (resolve, reject) {
25-
Client.connect(function () {
25+
const fsSocketClient = new FsSocketClient()
26+
fsSocketClient.connect(function () {
2627
connectedResolve(this)
2728
resolve(true)
29+
}, function (err) {
2830
})
2931
})
3032
}

app/components/FileTree/subscribeToFileChange.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
/* @flow weak */
2-
import config from '../../config'
3-
import api from '../../backendAPI'
4-
import store, { getState, dispatch } from '../../store'
5-
import mobxStore from '../../mobxStore'
2+
import config from 'config'
3+
import api from 'backendAPI'
4+
import { autorun } from 'mobx'
5+
import { FsSocketClient } from 'backendAPI/websocketClients'
6+
import store, { getState, dispatch } from 'store'
7+
import mobxStore from 'mobxStore'
8+
import * as TabActions from 'commons/Tab/actions'
69
import * as FileTreeActions from './actions'
710
import * as GitActions from '../Git/actions'
8-
import * as TabActions from 'commons/Tab/actions'
911

1012
function handleGitFiles (node) {
1113
const path = node.path
@@ -46,7 +48,13 @@ function handleGitFiles (node) {
4648
}
4749

4850
export default function subscribeToFileChange () {
49-
return api.websocketConnectedPromise.then(client => {
51+
autorun(() => {
52+
if (!config.fsSocketConnected) return
53+
const client = FsSocketClient.$$singleton.stompClient
54+
client.subscribe('CONNECTED', (frame) => {
55+
console.log('FS CONNECTED', frame);
56+
})
57+
5058
client.subscribe(`/topic/ws/${config.spaceKey}/change`, (frame) => {
5159
const data = JSON.parse(frame.body)
5260
const node = data.fileInfo

app/components/Modal/FilePalette/component.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import api from '../../../backendAPI'
55
import store, { dispatch as $d } from '../../../store'
66
import * as TabActions from 'commons/Tab/actions'
77
import cx from 'classnames'
8-
import { dispatchCommand } from '../../../commands/lib/keymapper'
8+
import dispatchCommand from 'commands/dispatchCommand'
99

1010
const debounced = _.debounce(function (func) { func() }, 1000)
1111

app/components/Terminal/Terminal.jsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import Terminal from 'sh.js';
55
import _ from 'lodash';
66
import { emitter, E } from 'utils'
77

8-
import terms from './terminal-client';
8+
import TerminalManager from './terminal-client';
99
import * as TabActions from 'commons/Tab/actions';
10-
terms.setActions(TabActions);
10+
1111

1212
class Term extends Component {
1313
constructor(props) {
@@ -17,26 +17,29 @@ class Term extends Component {
1717

1818
componentDidMount() {
1919
var _this = this;
20+
var terminalManager = new TerminalManager()
2021
var terminal = this.terminal = new Terminal({
2122
theme: 'terminal_basic',
2223
cols: 80,
2324
rows:24
2425
});
2526

27+
terminalManager.setActions(TabActions);
28+
2629
terminal.tabId = this.props.tab.id;
2730
terminal.open(this.termDOM);
28-
terminal.name = this.sessionId = _.uniqueId('term_');
31+
terminal.id = this.sessionId = _.uniqueId('term_');
2932

3033
terminal.on('resize', (cols, rows) => {
31-
terms.resize(terminal, cols, rows);
34+
terminalManager.resize(terminal, cols, rows);
3235
});
3336
setTimeout(() => terminal.sizeToFit(), 0)
3437
emitter.on(E.PANEL_RESIZED, this.onResize.bind(this))
3538
emitter.on(E.THEME_CHANGED, this.onTheme.bind(this))
3639

37-
terms.add(terminal);
40+
terminalManager.add(terminal);
3841
terminal.on('data', data => {
39-
terms.getSocket().emit('term.input', {id: terminal.name, input: data})
42+
terminalManager.getSocket().emit('term.input', {id: terminal.id, input: data})
4043
});
4144
terminal.on('title', _.debounce(title => {
4245
_this.props.tab.title = title

0 commit comments

Comments
 (0)