Skip to content

Commit a1af03f

Browse files
noVNC: fix JP keyboard on vmware7+ which uses websocket URL (#7694)
* noVNC: fix JP keyboard on vmware7+ which uses websocket URL * noVNC: cleanup rfb.js * noVNC: fix < and > on JP keyboard * noVNC: fix Caps lock on JP keyboard
1 parent 9df23f9 commit a1af03f

File tree

6 files changed

+1292
-2
lines changed

6 files changed

+1292
-2
lines changed

server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ private ConsoleEndpoint composeConsoleAccessEndpoint(String rootUrl, VirtualMach
385385
String token = encryptor.encryptObject(ConsoleProxyClientParam.class, param);
386386
int vncPort = consoleProxyManager.getVncPort();
387387

388-
String url = generateConsoleAccessUrl(rootUrl, param, token, vncPort, vm);
388+
String url = generateConsoleAccessUrl(rootUrl, param, token, vncPort, vm, hostVo, details);
389389

390390
s_logger.debug("Adding allowed session: " + sessionUuid);
391391
persistConsoleSession(sessionUuid, vm.getId(), hostVo.getId());
@@ -413,7 +413,7 @@ protected void persistConsoleSession(String sessionUuid, long instanceId, long h
413413
}
414414

415415
private String generateConsoleAccessUrl(String rootUrl, ConsoleProxyClientParam param, String token, int vncPort,
416-
VirtualMachine vm) {
416+
VirtualMachine vm, HostVO hostVo, UserVmDetailVO details) {
417417
StringBuilder sb = new StringBuilder(rootUrl);
418418
if (param.getHypervHost() != null || !ConsoleProxyManager.NoVncConsoleDefault.value()) {
419419
sb.append("/ajax?token=" + token);
@@ -422,6 +422,9 @@ private String generateConsoleAccessUrl(String rootUrl, ConsoleProxyClientParam
422422
.append("?autoconnect=true")
423423
.append("&port=" + vncPort)
424424
.append("&token=" + token);
425+
if (requiresVncOverWebSocketConnection(vm, hostVo) && details != null && details.getValue() != null) {
426+
sb.append("&language=" + details.getValue());
427+
}
425428
}
426429

427430
if (StringUtils.isNotBlank(param.getExtraSecurityToken())) {

systemvm/agent/noVNC/app/ui.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,7 @@ const UI = {
10771077
UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
10781078
{ shared: UI.getSetting('shared'),
10791079
repeaterID: UI.getSetting('repeaterID'),
1080+
language: WebUtil.getConfigVar('language'),
10801081
credentials: { password: password } });
10811082
UI.rfb.addEventListener("connect", UI.connectFinished);
10821083
UI.rfb.addEventListener("disconnect", UI.disconnectFinished);

systemvm/agent/noVNC/core/rfb.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import TightDecoder from "./decoders/tight.js";
3737
import TightPNGDecoder from "./decoders/tightpng.js";
3838
import ZRLEDecoder from "./decoders/zrle.js";
3939
import JPEGDecoder from "./decoders/jpeg.js";
40+
import SCANCODES_JP from "../keymaps/keymap-ja-atset1.js"
4041

4142
// How many seconds to wait for a disconnect to finish
4243
const DISCONNECT_TIMEOUT = 3;
@@ -118,8 +119,15 @@ export default class RFB extends EventTargetMixin {
118119
this._rfbCredentials = options.credentials || {};
119120
this._shared = 'shared' in options ? !!options.shared : true;
120121
this._repeaterID = options.repeaterID || '';
122+
this._language = options.language || '';
121123
this._wsProtocols = ['binary'];
122124

125+
// Keymaps
126+
this._scancodes = {};
127+
if (this._language === "jp") {
128+
this._scancodes = SCANCODES_JP;
129+
}
130+
123131
// Internal state
124132
this._rfbConnectionState = '';
125133
this._rfbInitState = '';
@@ -181,6 +189,10 @@ export default class RFB extends EventTargetMixin {
181189
encoding: null,
182190
};
183191

192+
// Keys
193+
this._shiftPressed = false;
194+
this._shiftKey = KeyTable.XK_Shift_L;
195+
184196
// Mouse state
185197
this._mousePos = {};
186198
this._mouseButtonMask = 0;
@@ -506,13 +518,43 @@ export default class RFB extends EventTargetMixin {
506518

507519
const scancode = XtScancode[code];
508520

521+
if (keysym === KeyTable.XK_Shift_L || keysym === KeyTable.XK_Shift_R) {
522+
this._shiftPressed = down;
523+
this._shiftKey = down ? keysym : KeyTable.XK_Shift_L;
524+
}
525+
509526
if (this._qemuExtKeyEventSupported && scancode) {
510527
// 0 is NoSymbol
511528
keysym = keysym || 0;
512529

513530
Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode);
514531

515532
RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode);
533+
} else if (Object.keys(this._scancodes).length > 0) {
534+
let vscancode = this._scancodes[keysym]
535+
if (vscancode) {
536+
let shifted = vscancode.includes("shift");
537+
let vscancode_int = parseInt(vscancode);
538+
let isLetter = (keysym >= 65 && keysym <=90) || (keysym >=97 && keysym <=122);
539+
if (shifted && ! this._shiftPressed && ! isLetter) {
540+
RFB.messages.keyEvent(this._sock, this._shiftKey, 1);
541+
}
542+
if (! shifted && this._shiftPressed && ! isLetter) {
543+
RFB.messages.keyEvent(this._sock, this._shiftKey, 0);
544+
}
545+
RFB.messages.VMwareExtendedKeyEvent(this._sock, keysym, down, vscancode_int);
546+
if (shifted && ! this._shiftPressed && ! isLetter) {
547+
RFB.messages.keyEvent(this._sock, this._shiftKey, 0);
548+
}
549+
if (! shifted && this._shiftPressed && ! isLetter) {
550+
RFB.messages.keyEvent(this._sock, this._shiftKey, 1);
551+
}
552+
} else {
553+
if (this._language === "jp" && keysym === 65328) {
554+
keysym = 65509; // Caps lock
555+
}
556+
RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
557+
}
516558
} else {
517559
if (!keysym) {
518560
return;
@@ -3031,6 +3073,26 @@ RFB.messages = {
30313073
sock.flush();
30323074
},
30333075

3076+
VMwareExtendedKeyEvent(sock, keysym, down, keycode) {
3077+
const buff = sock._sQ;
3078+
const offset = sock._sQlen;
3079+
3080+
buff[offset] = 127; // msgVMWClientMessage
3081+
buff[offset + 1] = 0; // msgVMWKeyEvent
3082+
3083+
buff[offset + 2] = 0;
3084+
buff[offset + 3] = 8;
3085+
3086+
buff[offset + 4] = (keycode >> 8);
3087+
buff[offset + 5] = keycode;
3088+
3089+
buff[offset + 6] = down;
3090+
buff[offset + 7] = 0;
3091+
3092+
sock._sQlen += 8;
3093+
sock.flush();
3094+
},
3095+
30343096
pointerEvent(sock, x, y, mask) {
30353097
const buff = sock._sQ;
30363098
const offset = sock._sQlen;
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#!/usr/bin/env python
2+
3+
# This script
4+
# (1) loads keysym name and keycode mappings from noVNC/core/input/keysym.js and
5+
# (2) loads keysyn name to atset1 code mappings from keymap files which can be downloadeded from https://github.com/qemu/qemu/blob/master/pc-bios/keymaps
6+
# (3) generates the mappings of keycode and atset1 code
7+
#
8+
# Note: please add language specific mappings if needed.
9+
10+
import os
11+
import sys
12+
13+
keycode_to_x11name = {}
14+
x11name_to_atset1 = {}
15+
layout = ""
16+
result_mappings = {}
17+
js_config = []
18+
19+
20+
def generate_x11name_to_atset1_map(keymap_file):
21+
# keymap files can be downloadeded from
22+
# https://github.com/qemu/qemu/blob/master/pc-bios/keymaps
23+
global layout
24+
try:
25+
for c in open(keymap_file):
26+
if len(c.strip()) == 0:
27+
continue
28+
if c.startswith("# quirks section start"):
29+
# Stop at the quirks section
30+
break
31+
if c.startswith("#"):
32+
if "layout" in c and ":" in c:
33+
layout = c.split(":")[1].strip()
34+
continue
35+
if "map " in c:
36+
continue
37+
values = c.strip().split(" ")
38+
if len(values) >= 2:
39+
keyname = 'XK_' + values[0]
40+
keycode = ' '.join(values[1:]).strip()
41+
if keyname not in x11name_to_atset1:
42+
x11name_to_atset1[keyname] = keycode
43+
else:
44+
print("Duplicated atset1 code for x11name '%s': '%s' and '%s'" % (
45+
keyname, x11name_to_atset1[keyname], keycode))
46+
47+
except IOError:
48+
print(
49+
"File '%s' does not exist. \nkeymaps can be downloaded from https://github.com/qemu/qemu/tree/master/pc-bios/keymaps" % keymap_file)
50+
sys.exit(2)
51+
52+
53+
def generate_keycode_to_x11name():
54+
for k in open("../core/input/keysym.js"):
55+
k = k.strip()
56+
if k.startswith("/") or k.startswith("*"):
57+
continue
58+
values = k.split(":")
59+
if len(values) >= 2:
60+
keyname = values[0]
61+
keycode = values[1].strip().split(",")[0].lower()
62+
keycode_int = int(keycode, 16)
63+
if str(keycode_int) not in keycode_to_x11name:
64+
keycode_to_x11name[str(keycode_int)] = keyname
65+
66+
67+
def translate(keysym):
68+
x11name = keycode_to_x11name[str(keysym)]
69+
if x11name not in x11name_to_atset1:
70+
print("Cannot find atset1 code for keysym (%s) / x11name (%s) in keymaps" % (keysym, x11name))
71+
return "", ""
72+
atset1 = x11name_to_atset1[x11name]
73+
values = atset1.split(" ")
74+
rest = ""
75+
if len(values) >= 2:
76+
atset1 = values[0]
77+
rest = " ".join(values[1:])
78+
return str(int(atset1, 16)), rest
79+
80+
81+
def add_language_specific_mappings():
82+
if layout == "jp":
83+
add_specific_mapping("124", "125", "shift") # bar
84+
85+
86+
def add_specific_mapping(keycode, atset1, rest):
87+
result_mappings[keycode] = "%s %s" % (str(atset1).ljust(10), rest)
88+
89+
90+
def generate_js_file(keymap_file):
91+
generated_filename = keymap_file + "-atset1.js"
92+
handle = open(generated_filename, "w")
93+
js_config.append("/* This file is auto-generated by generate-language-keymaps.py\n")
94+
js_config.append(" * command : %s\n" % (" ".join(sys.argv)))
95+
js_config.append(" * layout : %s\n" % layout)
96+
js_config.append(" */\n")
97+
js_config.append("export default {\n")
98+
for keycode in dict(sorted(result_mappings.items(), key=lambda item: int(item[0]))):
99+
js_config.append("%10s : \"%s\",\n" % ("\"" + str(keycode) + "\"", result_mappings[keycode].strip()))
100+
js_config.append("}")
101+
for line in js_config:
102+
handle.write(line)
103+
handle.close()
104+
print("Created file %s" % generated_filename)
105+
106+
107+
def main(argv):
108+
if len(argv) < 1:
109+
print("Usage: %s <keymap file>" % (sys.argv[0]))
110+
sys.exit(1)
111+
keymap_file = argv[0]
112+
generate_keycode_to_x11name()
113+
generate_x11name_to_atset1_map(keymap_file)
114+
115+
# insert language-specific mappings at beginning
116+
add_language_specific_mappings()
117+
118+
for keycode in keycode_to_x11name:
119+
atset1, rest = translate(keycode)
120+
if atset1 and keycode not in result_mappings and int(keycode) < 65000:
121+
result_mappings[keycode] = "%s %s" % (str(atset1).ljust(10), rest)
122+
123+
generate_js_file(keymap_file)
124+
125+
126+
if __name__ == "__main__":
127+
main(sys.argv[1:])

0 commit comments

Comments
 (0)