Skip to content

Commit

Permalink
Move kiosk project to pxt repo (#9624)
Browse files Browse the repository at this point in the history
* Move kiosk to pxt repo

* sync latest kiosk changes from arcade repo

* Move kiosk config files to arcade docs

* kiosk updates

* update GameDataUrl

* comment missing icon resources

* disable eslint, for now

* fix code scan warning
  • Loading branch information
eanders-ms authored Aug 9, 2023
1 parent d39643e commit 3854acb
Show file tree
Hide file tree
Showing 60 changed files with 30,188 additions and 7 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ webapp/public/closure-library
webapp/public/skillmap.html
webapp/public/authcode.html
webapp/public/multiplayer.html
webapp/public/kiosk.html
localtypings/blockly.d.ts
node_modules
*.sw?
Expand Down
24 changes: 23 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "serve",
"name": "pxt serve (microbit)",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/built/pxt.js",
Expand All @@ -23,6 +23,28 @@
"sourceMaps": false,
"outFiles": []
},
{
"name": "pxt serve (arcade)",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/built/pxt.js",
"stopOnEntry": false,
"args": [
"serve",
"--rebundle"
],
"cwd": "${workspaceRoot}/../pxt-arcade",
"runtimeExecutable": null,
"runtimeArgs": [
"--nolazy"
],
"env": {
"NODE_ENV": "development"
},
"console": "integratedTerminal",
"sourceMaps": false,
"outFiles": []
},
{
"name": "rebundle",
"type": "node",
Expand Down
17 changes: 14 additions & 3 deletions cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ function pxtFileList(pref: string) {
.concat(nodeutil.allFiles(pref + "built/web/skillmap", { maxDepth: 4 }))
.concat(nodeutil.allFiles(pref + "built/web/authcode", { maxDepth: 4 }))
.concat(nodeutil.allFiles(pref + "built/web/multiplayer", { maxDepth: 4 }))
.concat(nodeutil.allFiles(pref + "built/web/kiosk", { maxDepth: 4 }))
}

function semverCmp(a: string, b: string) {
Expand Down Expand Up @@ -456,7 +457,8 @@ function ciAsync() {
.then(() => crowdin.execCrowdinAsync("upload", "built/webstrings.json"))
.then(() => crowdin.execCrowdinAsync("upload", "built/skillmap-strings.json"))
.then(() => crowdin.execCrowdinAsync("upload", "built/authcode-strings.json"))
.then(() => crowdin.execCrowdinAsync("upload", "built/multiplayer-strings.json"));
.then(() => crowdin.execCrowdinAsync("upload", "built/multiplayer-strings.json"))
.then(() => crowdin.execCrowdinAsync("upload", "built/kiosk-strings.json"));
if (uploadApiStrings)
p = p.then(() => crowdin.execCrowdinAsync("upload", "built/strings.json"))
if (uploadDocs || uploadApiStrings)
Expand Down Expand Up @@ -1046,6 +1048,7 @@ function uploadCoreAsync(opts: UploadOptions) {
"skillmapUrl": opts.localDir + "skillmap.html",
"authcodeUrl": opts.localDir + "authcode.html",
"multiplayerUrl": opts.localDir + "multiplayer.html",
"kioskUrl": opts.localDir + "kiosk.html",
"isStatic": true,
}
const targetImageLocalPaths = targetImagePaths.map(k =>
Expand Down Expand Up @@ -1098,6 +1101,7 @@ function uploadCoreAsync(opts: UploadOptions) {
"skillmap.html",
"authcode.html",
"multiplayer.html",
"kiosk.html",
]

// expandHtml is manually called on these files before upload
Expand All @@ -1107,6 +1111,7 @@ function uploadCoreAsync(opts: UploadOptions) {
"skillmap.html",
"authcode.html",
"multiplayer.html",
"kiosk.html",
]

nodeutil.mkdirP("built/uploadrepl")
Expand Down Expand Up @@ -2000,7 +2005,7 @@ async function buildSemanticUIAsync(parsed?: commandParser.ParsedCommand) {
await writeFileAsync(`built/web/react-common-${app}.css`, appCss, "utf8");
}

// Generate react-common css for skillmap, authcode, and multiplayer
// Generate react-common css for skillmap, authcode, and multiplayer (but not kiosk yet)
await Promise.all([
generateReactCommonCss("skillmap"),
generateReactCommonCss("authcode"),
Expand Down Expand Up @@ -2028,7 +2033,13 @@ async function buildSemanticUIAsync(parsed?: commandParser.ParsedCommand) {
});

const rtlcss = require("rtlcss");
const files = ["semantic.css", "blockly.css", "react-common-skillmap.css", "react-common-authcode.css", "react-common-multiplayer.css"];
const files = [
"semantic.css",
"blockly.css",
"react-common-skillmap.css",
"react-common-authcode.css",
"react-common-multiplayer.css"
];

for (const cssFile of files) {
const css = await readFileAsync(`built/web/${cssFile}`, "utf8");
Expand Down
50 changes: 49 additions & 1 deletion cli/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ let packagedDir = ""
let localHexCacheDir = path.join("built", "hexcache");
let serveOptions: ServeOptions;

const webappNames = [
"kiosk"
// TODO: Add other webapp names here: "multiplayer", "skillmap", "authcode"
];

function setupDocfilesdirs() {
docfilesdirs = [
"docfiles",
Expand Down Expand Up @@ -987,7 +992,8 @@ export function serveAsync(options: ServeOptions) {
}
}

let pathname = decodeURI(url.parse(req.url).pathname);
let uri = url.parse(req.url);
let pathname = decodeURI(uri.pathname);
const opts: pxt.Map<string | string[]> = querystring.parse(url.parse(req.url).query);
const htmlParams: pxt.Map<string> = {};
if (opts["lang"] || opts["forcelang"])
Expand All @@ -1010,6 +1016,48 @@ export function serveAsync(options: ServeOptions) {
return error(400, "Bad path :-(\n")
}

// Strip leading version number
if (elts.length && /^v\d+/.test(elts[0])) {
elts.shift();
}

// Rebuild pathname without leading version number
pathname = "/" + elts.join("/");

const expandWebappHtml = (appname: string, html: string) => {
// Expand templates
html = expandHtml(html);
// Rewrite application resource references
html = html.replace(/src="(\/static\/js\/[^"]*)"/, (m, f) => `src="/${appname}${f}"`);
html = html.replace(/src="(\/static\/css\/[^"]*)"/, (m, f) => `src="/${appname}${f}"`);
return html;
};

const serveWebappFile = (webappName: string, webappPath: string) => {
const webappUri = url.parse(`http://localhost:3000/${webappPath}${uri.search || ""}`);
http.get(webappUri, r => {
let body = "";
r.on("data", (chunk) => {
body += chunk;
});
r.on("end", () => {
if (!webappPath || webappPath === "index.html") {
body = expandWebappHtml(webappName, body);
}
res.writeHead(200);
res.write(body);
res.end();
});
});
};

const webappIdx = webappNames.findIndex(s => new RegExp(`^-{0,3}${s}$`).test(elts[0] || ''));
if (webappIdx >= 0) {
const webappName = webappNames[webappIdx];
const webappPath = pathname.split("/").slice(2).join('/'); // remove /<webappName>/ from path
return serveWebappFile(webappName, webappPath);
}

if (elts[0] == "api") {
if (elts[1] == "streams") {
let trg = Cloud.apiRoot + req.url.slice(5)
Expand Down
35 changes: 34 additions & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ function updateMultiplayerStrings() {
return buildStrings("built/multiplayer-strings.json", ["multiplayer/src"], true);
}

function updateKioskStrings() {
return buildStrings("built/kiosk-strings.json", ["kiosk/src"], true);
}

// TODO: Copied from Jakefile; should be async
function buildStrings(out, rootPaths, recursive) {
let errCnt = 0;
Expand Down Expand Up @@ -653,6 +657,33 @@ const copyMultiplayerHtml = () => rimraf("webapp/public/multiplayer.html")

const multiplayer = gulp.series(cleanMultiplayer, buildMultiplayer, gulp.series(copyMultiplayerCss, copyMultiplayerJs, copyMultiplayerHtml));

/********************************************************
Kiosk
*********************************************************/

const kioskRoot = "kiosk";
const kioskOut = "built/web/kiosk";

const cleanKiosk = () => rimraf(kioskOut);

const npmBuildKiosk = () => exec("npm run build", true, { cwd: kioskRoot });

const buildKiosk = async () => await npmBuildKiosk();

const copyKioskCss = () => gulp.src(`${kioskRoot}/build/static/css/*`)
.pipe(gulp.dest(`${kioskOut}/css`));

const copyKioskJs = () => gulp.src(`${kioskRoot}/build/static/js/*`)
.pipe(gulp.dest(`${kioskOut}/js`));

const copyKioskHtml = () => rimraf("webapp/public/kiosk.html")
.then(() => gulp.src(`${kioskRoot}/build/index.html`)
.pipe(replace(/="\/static\//g, `="/blb/kiosk/`))
.pipe(concat("kiosk.html"))
.pipe(gulp.dest("webapp/public")));

const kiosk = gulp.series(cleanKiosk, buildKiosk, gulp.series(copyKioskCss, copyKioskJs, copyKioskHtml));

/********************************************************
Webapp build wrappers
*********************************************************/
Expand All @@ -665,12 +696,13 @@ const maybeUpdateWebappStrings = () => {
updateSkillMapStrings,
updateAuthcodeStrings,
updateMultiplayerStrings,
updateKioskStrings,
);
};

const maybeBuildWebapps = () => {
if (!shouldBuildWebapps()) return noop;
return gulp.parallel(skillmap, authcode, multiplayer);
return gulp.parallel(skillmap, authcode, multiplayer, kiosk);
}

/********************************************************
Expand Down Expand Up @@ -818,6 +850,7 @@ exports.onlinelearning = onlinelearning;
exports.skillmap = skillmap;
exports.authcode = authcode;
exports.multiplayer = multiplayer;
exports.kiosk = kiosk;
exports.icons = buildSVGIcons;
exports.testhelpers = testhelpers;
exports.cli = gulp.series(
Expand Down
3 changes: 3 additions & 0 deletions kiosk/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DISABLE_ESLINT_PLUGIN=true
GENERATE_SOURCEMAP=false
BROWSER=none
4 changes: 4 additions & 0 deletions kiosk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.vscode
build
!package-lock.json
src/Fonts.css
54 changes: 54 additions & 0 deletions kiosk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Kiosk

## config.json settings

These settings apply only to the kiosk user experience and do not impact sim gameplay. It's assumed all gamepads use the same configuration.

|Key|Purpose|
|---------------------------------|-|
|GameDataUrl | Path to the game data JSON file with details about the games to include.|
|PlayUrlRoot | Left part of the embedded player URL up to and including --run.|
|HighScoresToKeep | The maximum number of high scores to keep for a given game.|
|HighScoreInitialsLength | The number of initials to allow users to enter.|
|HighScoreInitialAllowedCharacters| The list and order of characters to allow in high scores.|
|GamepadPollLoopMilli | How often the gamepads are polled for user interaction with the kiosk menus and other inputs. If this is too large, actions may be missed, such as a quick button press. If it's too small, then a single action may be interpretted as multiple (like a single button press counting twice).|
|GamepadCacheMilli | How often the gamepad state is cached. Since multiple components access the browser's Navigator.getGamepads() API, we use a caching mechanism to optimize how often they're accessed. This value should be smaller than the gamepad polling loop.|
|GamepadAButtonPin | The pin index for the A button. |
|GamepadBButtonPin | The pin index for the B button.|
|GamepadEscapeButtonPin | The pin index for the Escape button. This button leaves a game without waiting and returns the user to the menu.|
|GamepadResetButtonPin | The pin index for the Reset button.|
|GamepadMenuButtonPin | The pin index for the Menu button. Note that since this menu is used by games, it does not return the user to the kiosk menu. |
|GamepadLeftRightAxis | The gamepad axis index for left/right detection.|
|GamepadLeftRightThreshold | The threshold to detect for a "right" action. It's negated to detect "left" actions.|
|GamepadUpDownAxis | The gamepad axis index for up/down detection.|
|GamepadUpDownThreshold | The threshold to detect for a "down" action. It's negated to detect "up" actions.|
|Keyboard`Input`Keys | Each is an array of string arrays matching the browser keys that map to the target behavior. The index of a list maps to the same gamepad index (player 1 is the first array at index 0, etc.) |

## Localhost testing

To test Kiosk locally:

1. Ensure your pxt repo is up to date and has been built recently.
2. In a command shell, in the `pxt` repo, cd into the `kiosk` folder and start the Kiosk dev server: `npm run start`. This will *not* open a browser window.
3. In another command shell, in the `pxt-arcade` repo, start the Arcade dev server: `pxt serve --rebundle`. This will open the Arcade webapp in a browser.

Requests to the `/kiosk` endpoint will be routed to the Kiosk dev server.

Debug and step through Kiosk code using the browser dev tools (F12 to open).


## Test in staging environment

1. In the pxt repo, run `gulp` to ensure production kiosk is built.
2. In a browser, go to `https://staging.pxt.io/oauth/gettoken`. This should return a url with an auth token embedded. Copy the entire url value to your clipboard.
- It should look something like `https://staging.pxt.io/?access_token=X.XXXXXXXX`
- If you get access denied, contact your manager to help you.
3. In a command shell, set environment variable `PXT_ACCESS_TOKEN` with the copied value.
4. In the same shell, in the pxt-arcade repo, run `pxt uploadtrg --rebundle`. This should return a url to your private build.
- It should look something like `https://arcade.staging.pxt.io/app/XXXXXX-XXXXX`
- Paste in a browser and append "/kiosk". This should take you to your kiosk build in staging.

## Test in production environment

Follow the "Test in staging environment" instructions, but get your auth token from `https://makecode.com/oauth/gettoken`.

43 changes: 43 additions & 0 deletions kiosk/config-overrides.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const { aliasWebpack } = require("react-app-alias");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = function (config, env) {
const isEnvProduction = env === "production";
const aliasFn = aliasWebpack({});
config = {
...aliasFn(config),
plugins: [
...config.plugins.filter((p) => !(p instanceof HtmlWebpackPlugin)),
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: "./public/index.html",
},
isEnvProduction
? {
minify: {
removeComments: false,
collapseWhitespace: false,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
],
module: {
...config.module,
rules: config.module.rules.filter(el => !(el.use && el.use.some(item => !!(item.options && item.options.eslintPath)))),
}
};
return config;
};
Loading

0 comments on commit 3854acb

Please sign in to comment.