Skip to content

Commit 0e05bf3

Browse files
committed
Ask for a reload when the live view drops, and only say shut down when it is
1 parent d169d23 commit 0e05bf3

7 files changed

Lines changed: 165 additions & 4 deletions

File tree

‎images/chromium-headful/client/src/components/disconnected.vue‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<template>
22
<div class="disconnected-overlay">
33
<div class="message-container">
4-
<span class="message">{{ $t('disconnected.message') }}</span>
4+
<span class="message">{{ sessionGone ? $t('disconnected.gone') : $t('disconnected.lost') }}</span>
5+
<button v-if="!sessionGone" class="reload" @click="reload">{{ $t('disconnected.reload') }}</button>
56
</div>
67
</div>
78
</template>
@@ -17,15 +18,30 @@
1718
1819
.message-container {
1920
display: flex;
21+
flex-direction: column;
2022
justify-content: center;
2123
align-items: center;
24+
gap: 16px;
2225
padding: 20px;
2326
2427
.message {
2528
font-size: 18px;
2629
color: $text-muted;
2730
text-align: center;
2831
}
32+
33+
.reload {
34+
min-height: 44px;
35+
padding: 0 20px;
36+
border: 0;
37+
border-radius: 22px;
38+
background: rgba($color: #fff, $alpha: 0.15);
39+
color: #fff;
40+
font-size: 15px;
41+
font-weight: 600;
42+
cursor: pointer;
43+
touch-action: manipulation;
44+
}
2945
}
3046
}
3147
</style>
@@ -34,5 +50,16 @@
3450
import { Component, Vue } from 'vue-property-decorator'
3551
3652
@Component({ name: 'neko-disconnected' })
37-
export default class extends Vue {}
53+
export default class extends Vue {
54+
// Only a session-gone close means the browser is gone. Anything else may be
55+
// a network change with the browser still running, so offer a reload rather
56+
// than claiming it shut down.
57+
get sessionGone() {
58+
return this.$accessor.sessionGone
59+
}
60+
61+
reload() {
62+
location.reload()
63+
}
64+
}
3865
</script>

‎images/chromium-headful/client/src/locale/en-us.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,5 +127,7 @@ export const files = {
127127
}
128128

129129
export const disconnected = {
130-
message: 'Browser has been shut down and is no longer available',
130+
gone: 'Browser has been shut down and is no longer available',
131+
lost: 'Lost connection to the browser',
132+
reload: 'Reload',
131133
}

‎images/chromium-headful/client/src/neko/base.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ export const CONNECT_STAGE_TIMEOUT_MS = resolveConnectStageTimeoutMs(
8282
// prose. The three stages mean a bound expired; the rest never reached one.
8383
export type ConnectFailure = ConnectStage | 'unsupported' | 'peer' | 'server'
8484

85+
// The live-view proxy closes the socket with this code when the browser session
86+
// no longer exists. Any other close leaves the browser possibly still running.
87+
export const SESSION_GONE_CLOSE_CODE = 4410
88+
8589
export interface BaseEvents {
8690
info: (...message: any[]) => void
8791
warn: (...message: any[]) => void
@@ -101,6 +105,7 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
101105
protected _stage?: ConnectStage
102106
protected _everConnected = false
103107
protected _gaveUp = false
108+
protected _sessionGone = false
104109
// Tagged where the failure happens, so a pre-connect disconnect is reported
105110
// with a reason an embedder can act on. Unset falls back to 'peer'.
106111
protected _failure?: ConnectFailure
@@ -129,6 +134,10 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
129134
return this.peerConnected && this.socketOpen
130135
}
131136

137+
get sessionGone() {
138+
return this._sessionGone
139+
}
140+
132141
public connect(url: string, password: string, displayname: string) {
133142
if (this._gaveUp) {
134143
this.emit('debug', `not reconnecting, already gave up`)
@@ -162,6 +171,7 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
162171
this._ws.onopen = () => this.armStage('signaling')
163172
this._ws.onclose = (event) => {
164173
this.emit('debug', `websocket closed: code=${event.code}, reason=${event.reason}`)
174+
this._sessionGone = event.code === SESSION_GONE_CLOSE_CODE
165175
this._failure = 'transport'
166176
this.onDisconnected(new Error('websocket closed'))
167177
}

‎images/chromium-headful/client/src/neko/index.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
111111

112112
protected [EVENT.DISCONNECTED](reason?: Error) {
113113
this.cleanup()
114+
this.$accessor.setSessionGone(this.sessionGone)
114115

115116
this.$vue.$notify({
116117
group: 'neko',
@@ -166,6 +167,14 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
166167
}
167168

168169
protected [EVENT.SYSTEM.DISCONNECT]({ message }: SystemMessagePayload) {
170+
// neko sends this as it stops, on a restart and on browser teardown alike,
171+
// so it does not say whether the browser is gone. The socket close that
172+
// follows does: the live-view proxy closes with the session-gone code once
173+
// the session has ended.
174+
if (message == 'connection shutdown') {
175+
return
176+
}
177+
169178
if (message == 'kicked') {
170179
this.$accessor.logout()
171180
message = this.$vue.$t('connection.kicked') as string

‎images/chromium-headful/client/src/store/index.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const state = () => ({
2020
active: false,
2121
connecting: false,
2222
connected: false,
23+
sessionGone: false,
2324
locked: {} as Record<string, boolean>,
2425
})
2526

@@ -46,6 +47,10 @@ export const mutations = mutationTree(state, {
4647
state.connecting = true
4748
},
4849

50+
setSessionGone(state, sessionGone: boolean) {
51+
state.sessionGone = sessionGone
52+
},
53+
4954
setConnected(state, connected: boolean) {
5055
state.connected = connected
5156
state.connecting = false

‎images/chromium-headful/client/tests/connect-bound.test.ts‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2-
import { BaseClient, CONNECT_STAGE_TIMEOUT_MS, resolveConnectStageTimeoutMs } from '../src/neko/base'
2+
import {
3+
BaseClient,
4+
CONNECT_STAGE_TIMEOUT_MS,
5+
SESSION_GONE_CLOSE_CODE,
6+
resolveConnectStageTimeoutMs,
7+
} from '../src/neko/base'
38

49
const posted: Record<string, any>[] = []
510
const timers: { id: number; delay: number; run: () => void }[] = []
@@ -454,3 +459,38 @@ describe('live view connect failures', () => {
454459
})
455460
})
456461
})
462+
463+
// A connected viewer that drops is not reconnected: the overlay asks for a reload.
464+
// Only the proxy's session-gone close means the browser itself is gone.
465+
describe('live view drops after connecting', () => {
466+
async function connectedClient() {
467+
const client = new TestClient()
468+
client.connect('ws://host/ws', 'pw', 'kernel')
469+
client.openSocket()
470+
await client.provide()
471+
client.connectPeer()
472+
return client
473+
}
474+
475+
test('a session-gone close marks the session gone and opens no new socket', async () => {
476+
const client = await connectedClient()
477+
478+
client['_ws']!.onclose({ code: SESSION_GONE_CLOSE_CODE, reason: '' } as CloseEvent)
479+
480+
expect(client.sessionGone).toBe(true)
481+
expect(client['_ws']).toBeUndefined()
482+
expect(timers).toEqual([])
483+
expect(posted).toEqual([])
484+
})
485+
486+
test('any other close leaves the session not known to be gone', async () => {
487+
const client = await connectedClient()
488+
489+
client['_ws']!.onclose({ code: 1006, reason: '' } as CloseEvent)
490+
491+
expect(client.sessionGone).toBe(false)
492+
expect(client['_ws']).toBeUndefined()
493+
expect(timers).toEqual([])
494+
expect(client.reasons.map((r) => r?.message)).toEqual(['websocket closed'])
495+
})
496+
})
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { beforeEach, describe, expect, mock, test } from 'bun:test'
2+
3+
mock.module('~/store', () => ({ accessor: {} }))
4+
const { NekoClient } = await import('../src/neko/index')
5+
6+
const reset = { reset: mock() }
7+
let accessor: Record<string, any>
8+
9+
function client() {
10+
const neko = new NekoClient()
11+
neko['$accessor'] = accessor as any
12+
neko['$vue'] = { $notify: mock(), $swal: mock(), $t: (key: string) => key } as any
13+
return neko
14+
}
15+
16+
beforeEach(() => {
17+
accessor = {
18+
setConnected: mock(),
19+
setSessionGone: mock(),
20+
remote: reset,
21+
user: reset,
22+
video: reset,
23+
chat: reset,
24+
}
25+
})
26+
27+
// The overlay says the browser shut down only when the proxy said so.
28+
describe('disconnect overlay state', () => {
29+
test('a session-gone close is recorded as gone', () => {
30+
const neko = client()
31+
neko['_sessionGone'] = true
32+
33+
neko['DISCONNECTED'](new Error('websocket closed'))
34+
35+
expect(accessor.setSessionGone).toHaveBeenCalledWith(true)
36+
})
37+
38+
test('any other disconnect is recorded as not gone', () => {
39+
const neko = client()
40+
41+
neko['DISCONNECTED'](new Error('peer failed'))
42+
43+
expect(accessor.setSessionGone).toHaveBeenCalledWith(false)
44+
})
45+
})
46+
47+
describe('server disconnect messages', () => {
48+
test('neko shutting down is left to the socket close', () => {
49+
const neko = client()
50+
neko['onDisconnected'] = mock()
51+
52+
neko['system/disconnect']({ message: 'connection shutdown' })
53+
54+
expect(neko['_failure']).toBeUndefined()
55+
expect(neko['onDisconnected']).not.toHaveBeenCalled()
56+
expect(neko['$vue'].$swal).not.toHaveBeenCalled()
57+
})
58+
59+
test('any other reason ends the viewer as a server disconnect', () => {
60+
const neko = client()
61+
neko['onDisconnected'] = mock()
62+
63+
neko['system/disconnect']({ message: 'connection replaced' })
64+
65+
expect(neko['_failure']).toBe('server')
66+
expect(neko['onDisconnected']).toHaveBeenCalledTimes(1)
67+
})
68+
})

0 commit comments

Comments
 (0)