Correctly interpret string arguments as booleans in electron arguments. (https://github.com/enso-org/ide/pull/1539)

Original commit: f0e712e45c
This commit is contained in:
Michael Mauderer 2021-05-10 19:23:22 +02:00 committed by GitHub
parent 6fd49b26c8
commit b7baae57ff
6 changed files with 268 additions and 167 deletions

View File

@ -1,6 +1,6 @@
overrides:
- files: "*.js"
- files: "*.[j|t]s"
options:
printWidth: 100
tabWidth: 4

View File

@ -44,6 +44,10 @@ you can find their release notes
<br/>![Bug Fixes](/docs/assets/tags/bug_fixes.svg)
- [Fix some internal settings not being applied correctly in the IDE][1539].
Some arguments were not passed correctly to the IDE leading to erroneous
behaviour in the electron app. This is now fixed.
#### Visual Environment
- [Some command line arguments were not applied correctly in the IDE][1536].
@ -59,6 +63,7 @@ you can find their release notes
[1511]: https://github.com/enso-org/ide/pull/1511
[1536]: https://github.com/enso-org/ide/pull/1536
[1531]: https://github.com/enso-org/ide/pull/1531
[1531]: https://github.com/enso-org/ide/pull/1539
<br/>

View File

@ -14,6 +14,10 @@ let config = {
"compression-webpack-plugin": "^3.1.0",
"copy-webpack-plugin": "^5.1.1",
"yaml-loader": "^0.6.0",
"ts-loader": "^8.0.3",
"typescript": "^4.0.2",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12"
}
}

View File

@ -2,14 +2,16 @@
/// user with a visual representation of this process (welcome screen). It also implements a view
/// allowing to choose a debug rendering test from.
// @ts-ignore
import * as loader_module from 'enso-studio-common/src/loader'
import * as html_utils from 'enso-studio-common/src/html_utils'
import * as animation from 'enso-studio-common/src/animation'
import * as globalConfig from '../../../../config.yaml'
import cfg from '../../../config'
import assert from "assert";
// @ts-ignore
import * as html_utils from 'enso-studio-common/src/html_utils'
// @ts-ignore
import * as globalConfig from '../../../../config.yaml'
// @ts-ignore
import cfg from '../../../config'
// @ts-ignore
import assert from 'assert'
// =================
// === Constants ===
@ -17,17 +19,32 @@ import assert from "assert";
const ALIVE_LOG_INTERVAL = 1000 * 60
// ==================
// === Global API ===
// ==================
let API = {}
class ContentApi {
main: (inputConfig: any) => Promise<void>
private logger: MixpanelLogger
initLogging(config: Config) {
assert(typeof config.no_data_gathering == 'boolean')
if (!config.no_data_gathering) {
this.logger = new MixpanelLogger()
}
}
remoteLog(event: string, data?: any) {
if (this.logger) {
this.logger.log(event, data)
}
}
}
const API = new ContentApi()
// @ts-ignore
window[globalConfig.windowAppScopeName] = API
// ========================
// === Content Download ===
// ========================
@ -37,27 +54,29 @@ let incorrect_mime_type_warning = `
'application/wasm' MIME type. Falling back to 'WebAssembly.instantiate' which is slower.
`
function wasm_instantiate_streaming(resource,imports) {
return WebAssembly.instantiateStreaming(resource,imports).catch(e => {
return wasm_fetch.then(r => {
if (r.headers.get('Content-Type') != 'application/wasm') {
console.warn(`${incorrect_mime_type_warning} Original error:\n`, e)
return r.arrayBuffer()
} else {
throw("Server not configured to serve WASM with 'application/wasm' mime type.")
}
}).then(bytes => WebAssembly.instantiate(bytes,imports))
})
async function wasm_instantiate_streaming(
resource: Response,
imports: WebAssembly.Imports
): Promise<ArrayBuffer | WebAssembly.WebAssemblyInstantiatedSource> {
try {
return WebAssembly.instantiateStreaming(resource, imports)
} catch (e) {
const r = await resource
if (r.headers.get('Content-Type') != 'application/wasm') {
console.warn(`${incorrect_mime_type_warning} Original error:\n`, e)
return r.arrayBuffer()
} else {
throw "Server not configured to serve WASM with 'application/wasm' mime type."
}
}
}
/// Downloads the WASM binary and its dependencies. Displays loading progress bar unless provided
/// with `{use_loader:false}` option.
async function download_content(config) {
async function download_content(config: { wasm_glue_url: RequestInfo; wasm_url: RequestInfo }) {
let wasm_glue_fetch = await fetch(config.wasm_glue_url)
let wasm_fetch = await fetch(config.wasm_url)
let loader =
new loader_module.Loader([wasm_glue_fetch,wasm_fetch], config)
let wasm_fetch = await fetch(config.wasm_url)
let loader = new loader_module.Loader([wasm_glue_fetch, wasm_fetch], config)
// TODO [mwu]
// Progress indication for WASM loading is hereby capped at 30%.
@ -69,35 +88,35 @@ async function download_content(config) {
loader.done.then(() => {
console.groupEnd()
console.log("Download finished. Finishing WASM compilation.")
console.log('Download finished. Finishing WASM compilation.')
})
let download_size = loader.show_total_bytes()
let download_info = `Downloading WASM binary and its dependencies (${download_size}).`
let wasm_loader = html_utils.log_group_collapsed(download_info, async () => {
let wasm_loader = html_utils.log_group_collapsed(download_info, async () => {
let wasm_glue_js = await wasm_glue_fetch.text()
let wasm_glue = Function("let exports = {};" + wasm_glue_js + "; return exports")()
let imports = wasm_glue.wasm_imports()
console.log("WASM dependencies loaded.")
console.log("Starting online WASM compilation.")
let wasm_loader = await wasm_instantiate_streaming(wasm_fetch,imports)
let wasm_glue = Function('let exports = {};' + wasm_glue_js + '; return exports')()
let imports = wasm_glue.wasm_imports()
console.log('WASM dependencies loaded.')
console.log('Starting online WASM compilation.')
let wasm_loader = await wasm_instantiate_streaming(wasm_fetch, imports)
// @ts-ignore
wasm_loader.wasm_glue = wasm_glue
return wasm_loader
})
let wasm = await wasm_loader.then(({instance,module,wasm_glue}) => {
// @ts-ignore
let wasm = await wasm_loader.then(({ instance, module, wasm_glue }) => {
let wasm = instance.exports
wasm_glue.after_load(wasm,module)
wasm_glue.after_load(wasm, module)
return wasm
})
console.log("WASM Compiled.")
console.log('WASM Compiled.')
await loader.initialized
return {wasm,loader}
return { wasm, loader }
}
// ====================
// === Debug Screen ===
// ====================
@ -106,46 +125,44 @@ async function download_content(config) {
let main_entry_point = 'ide'
/// Prefix name of each scene defined in the WASM binary.
let wasm_entry_point_pfx = "entry_point_"
let wasm_entry_point_pfx = 'entry_point_'
/// Displays a debug screen which allows the user to run one of predefined debug examples.
function show_debug_screen(wasm,msg) {
API.remoteLog("show_debug_screen")
function show_debug_screen(wasm: any, msg: string) {
API.remoteLog('show_debug_screen')
let names = []
for (let fn of Object.getOwnPropertyNames(wasm)) {
if (fn.startsWith(wasm_entry_point_pfx)) {
let name = fn.replace(wasm_entry_point_pfx,"")
let name = fn.replace(wasm_entry_point_pfx, '')
names.push(name)
}
}
if(msg==="" || msg===null || msg===undefined) { msg = "" }
if (msg === '' || msg === null || msg === undefined) {
msg = ''
}
let debug_screen_div = html_utils.new_top_level_div()
let newDiv = document.createElement("div")
let newContent = document.createTextNode(msg + "Available entry points:")
let currentDiv = document.getElementById("app")
let ul = document.createElement('ul')
let newDiv = document.createElement('div')
let newContent = document.createTextNode(msg + 'Available entry points:')
let ul = document.createElement('ul')
debug_screen_div.style.position = 'absolute'
debug_screen_div.style.zIndex = 1
debug_screen_div.style.zIndex = 1
newDiv.appendChild(newContent)
debug_screen_div.appendChild(newDiv)
newDiv.appendChild(ul)
for (let name of names) {
let li = document.createElement('li')
let a = document.createElement('a')
let li = document.createElement('li')
let a = document.createElement('a')
let linkText = document.createTextNode(name)
ul.appendChild(li)
a.appendChild(linkText)
a.title = name
a.href = "?entry="+name
a.title = name
a.href = '?entry=' + name
li.appendChild(a)
}
}
// ====================
// === Scam Warning ===
// ====================
@ -159,52 +176,58 @@ function printScamWarning() {
font-weight : bold;
padding: 10px 20px 10px 20px;
`
let headerCSS1 = headerCSS + "font-size : 46px;"
let headerCSS2 = headerCSS + "font-size : 20px;"
let msgCSS = "font-size:16px;"
let headerCSS1 = headerCSS + 'font-size : 46px;'
let headerCSS2 = headerCSS + 'font-size : 20px;'
let msgCSS = 'font-size:16px;'
let msg1 = "This is a browser feature intended for developers. If someone told you to " +
"copy-paste something here, it is a scam and will give them access to your " +
"account and data."
let msg2 = "See https://github.com/enso-org/ide/blob/main/docs/security/selfxss.md for more " +
"information."
console.log("%cStop!",headerCSS1)
console.log("%cYou may be victim of a scam!",headerCSS2)
console.log("%c"+msg1,msgCSS)
console.log("%c"+msg2,msgCSS)
let msg1 =
'This is a browser feature intended for developers. If someone told you to ' +
'copy-paste something here, it is a scam and will give them access to your ' +
'account and data.'
let msg2 =
'See https://github.com/enso-org/ide/blob/main/docs/security/selfxss.md for more ' +
'information.'
console.log('%cStop!', headerCSS1)
console.log('%cYou may be victim of a scam!', headerCSS2)
console.log('%c' + msg1, msgCSS)
console.log('%c' + msg2, msgCSS)
}
// ======================
// === Remote Logging ===
// ======================
class MixpanelLogger {
private readonly mixpanel: any
constructor() {
this.mixpanel = require('mixpanel-browser');
this.mixpanel.init("5b541aeab5e08f313cdc1d1bbebc12ac", { "api_host": "https://api-eu.mixpanel.com" }, "");
this.mixpanel = require('mixpanel-browser')
this.mixpanel.init(
'5b541aeab5e08f313cdc1d1bbebc12ac',
{ api_host: 'https://api-eu.mixpanel.com' },
''
)
}
log(event,data) {
log(event: string, data: any) {
if (this.mixpanel) {
event = MixpanelLogger.trim_message(event)
if (data !== undefined && data !== null) {
data = MixpanelLogger.trim_message(JSON.stringify(data))
this.mixpanel.track(event,{data});
this.mixpanel.track(event, { data })
} else {
this.mixpanel.track(event);
this.mixpanel.track(event)
}
} else {
console.warn(`Failed to log the event '${event}'.`)
}
}
static trim_message(message) {
const MAX_MESSAGE_LENGTH = 500;
let trimmed = message.substr(0,MAX_MESSAGE_LENGTH)
static trim_message(message: string) {
const MAX_MESSAGE_LENGTH = 500
let trimmed = message.substr(0, MAX_MESSAGE_LENGTH)
if (trimmed.length < message.length) {
trimmed += "..."
trimmed += '...'
}
return trimmed
}
@ -214,36 +237,46 @@ class MixpanelLogger {
// === Logs Buffering ===
// ======================
const logsFns = ['log','info','debug','warn','error','group','groupCollapsed','groupEnd']
const logsFns = ['log', 'info', 'debug', 'warn', 'error', 'group', 'groupCollapsed', 'groupEnd']
class LogRouter {
private buffer: any[]
private readonly raw: {}
autoFlush: boolean
constructor() {
this.buffer = []
this.raw = {}
this.buffer = []
this.raw = {}
this.autoFlush = true
// @ts-ignore
console.autoFlush = true
for (let name of logsFns) {
// @ts-ignore
this.raw[name] = console[name]
// @ts-ignore
console[name] = (...args) => {
this.handle(name,args)
this.handle(name, args)
}
}
}
auto_flush_on() {
this.autoFlush = true
// @ts-ignore
console.autoFlush = true
for (let {name,args} of this.buffer) {
for (let { name, args } of this.buffer) {
// @ts-ignore
this.raw[name](...args)
}
this.buffer = []
}
handle(name,args) {
handle(name: string, args: any[]) {
if (this.autoFlush) {
// @ts-ignore
this.raw[name](...args)
} else {
this.buffer.push({name,args})
this.buffer.push({ name, args })
}
// The following code is just a hack to discover if the logs start with `[E]` which
@ -271,8 +304,8 @@ class LogRouter {
}
}
handleError(...args) {
API.remoteLog("error", args)
handleError(...args: any[]) {
API.remoteLog('error', args)
}
}
@ -281,6 +314,7 @@ let logRouter = new LogRouter()
function hideLogs() {
console.log('All subsequent logs will be hidden. Eval `showLogs()` to reveal them.')
logRouter.autoFlush = false
// @ts-ignore
console.autoFlush = false
}
@ -288,10 +322,9 @@ function showLogs() {
logRouter.auto_flush_on()
}
// @ts-ignore
window.showLogs = showLogs
// ======================
// === Crash Handling ===
// ======================
@ -304,7 +337,7 @@ function initCrashHandling() {
}
}
const crashMessageStorageKey = "crash-message"
const crashMessageStorageKey = 'crash-message'
function previousCrashMessageExists() {
return sessionStorage.getItem(crashMessageStorageKey) !== null
@ -314,7 +347,7 @@ function getPreviousCrashMessage() {
return sessionStorage.getItem(crashMessageStorageKey)
}
function storeLastCrashMessage(message) {
function storeLastCrashMessage(message: string) {
sessionStorage.setItem(crashMessageStorageKey, message)
}
@ -322,7 +355,6 @@ function clearPreviousCrashMessage() {
sessionStorage.removeItem(crashMessageStorageKey)
}
// === Crash detection ===
function setupCrashDetection() {
@ -341,38 +373,42 @@ function setupCrashDetection() {
window.addEventListener('unhandledrejection', function (event) {
// As above, we prefer stack traces.
// But here, `event.reason` is not even guaranteed to be an `Error`.
handleCrash(event.reason.stack || event.reason.message || "Unhandled rejection")
handleCrash(event.reason.stack || event.reason.message || 'Unhandled rejection')
})
}
function handleCrash(message) {
API.remoteLog("crash", message)
function handleCrash(message: string) {
API.remoteLog('crash', message)
if (document.getElementById(crashBannerId) === null) {
storeLastCrashMessage(message)
location.reload()
} else {
for (let element of [... document.body.childNodes]) {
// @ts-ignore
for (let element of [...document.body.childNodes]) {
// @ts-ignore
if (element.id !== crashBannerId) {
element.remove()
}
}
document.getElementById(crashBannerContentId).insertAdjacentHTML("beforeend",
document.getElementById(crashBannerContentId).insertAdjacentHTML(
'beforeend',
`<hr>
<div>A second error occurred. This time, the IDE will not automatically restart.</div>`)
<div>A second error occurred. This time, the IDE will not automatically restart.</div>`
)
}
}
// === Crash recovery ===
// Those IDs should be the same that are used in index.html.
const crashBannerId = "crash-banner"
const crashBannerContentId = "crash-banner-content"
const crashReportButtonId = "crash-report-button"
const crashBannerCloseButtonId = "crash-banner-close-button"
const crashBannerId = 'crash-banner'
const crashBannerContentId = 'crash-banner-content'
const crashReportButtonId = 'crash-report-button'
const crashBannerCloseButtonId = 'crash-banner-close-button'
function showCrashBanner(message) {
document.body.insertAdjacentHTML('afterbegin',
function showCrashBanner(message: string) {
document.body.insertAdjacentHTML(
'afterbegin',
`<div id="${crashBannerId}">
<button id="${crashBannerCloseButtonId}" class="icon-button"></button>
<div id="${crashBannerContentId}">
@ -390,9 +426,9 @@ function showCrashBanner(message) {
report_button.onclick = async _event => {
try {
await reportCrash(message)
content.textContent = "Thank you, the crash was reported."
content.textContent = 'Thank you, the crash was reported.'
} catch (e) {
content.textContent = "The crash could not be reported."
content.textContent = 'The crash could not be reported.'
}
}
close_button.onclick = () => {
@ -400,20 +436,19 @@ function showCrashBanner(message) {
}
}
async function reportCrash(message) {
async function reportCrash(message: string) {
// @ts-ignore
const crashReportHost = API[globalConfig.windowAppScopeConfigName].crash_report_host
await fetch(`http://${crashReportHost}/`, {
method: 'POST',
mode: 'no-cors',
headers: {
'Content-Type': 'text/plain'
'Content-Type': 'text/plain',
},
body: message
})
body: message,
})
}
// ========================
// === Main Entry Point ===
// ========================
@ -426,6 +461,7 @@ function style_root() {
/// Waits for the window to finish its show animation. It is used when the website is run in
/// Electron. Please note that it returns immediately in the web browser.
async function windowShowAnimation() {
// @ts-ignore
await window.showAnimation
}
@ -435,63 +471,104 @@ function disableContextMenu() {
})
}
function ok(value) {
function ok(value: any) {
return value !== null && value !== undefined
}
class Config {
public use_loader: boolean
public wasm_url: string
public wasm_glue_url: string
public crash_report_host: string
public no_data_gathering: boolean
public is_in_cloud: boolean
public entry: string
static default() {
let config = new Config()
config.use_loader = true
config.wasm_url = '/assets/ide.wasm'
config.wasm_glue_url = '/assets/wasm_imports.js'
config.crash_report_host = cfg.defaultLogServerHost
config.no_data_gathering = false
config.is_in_cloud = false
config.entry = null
return config
}
updateFromObject(other: any) {
if (!ok(other)) {
return
}
this.use_loader = ok(other.use_loader) ? tryAsBoolean(other.use_loader) : this.use_loader
this.no_data_gathering = ok(other.no_data_gathering)
? tryAsBoolean(other.no_data_gathering)
: this.no_data_gathering
this.is_in_cloud = ok(other.is_in_cloud)
? tryAsBoolean(other.is_in_cloud)
: this.is_in_cloud
this.wasm_url = ok(other.wasm_url) ? tryAsString(other.wasm_url) : this.wasm_url
this.wasm_glue_url = ok(other.wasm_glue_url)
? tryAsString(other.wasm_glue_url)
: this.wasm_glue_url
this.crash_report_host = ok(other.crash_report_host)
? tryAsString(other.crash_report_host)
: this.crash_report_host
this.entry = ok(other.entry) ? tryAsString(other.entry) : this.entry
}
}
/// Check whether the value is a string with value `"true"`/`"false"`, if so, return the
// appropriate boolean instead. Otherwise, return the original value.
function parseBooleanOrLeaveAsIs(value) {
if (value === "true"){
function parseBooleanOrLeaveAsIs(value: any): any {
if (value === 'true') {
return true
}
if (value === "false"){
if (value === 'false') {
return false
}
return value
}
/// Turn all values that have a boolean in string representation (`"true"`/`"false"`) into actual
/// booleans (`true/`false``).
function parseAllBooleans(config) {
for (const key in config) {
config[key] = parseBooleanOrLeaveAsIs(config[key])
}
function tryAsBoolean(value: any): boolean {
value = parseBooleanOrLeaveAsIs(value)
assert(typeof value == 'boolean')
return value
}
function initLogging(config) {
assert(typeof config.no_data_gathering == "boolean")
if (config.no_data_gathering ) {
API.remoteLog = function (_event, _data) {}
} else {
let logger = new MixpanelLogger
API.remoteLog = function (event,data) {logger.log(event,data)}
}
function tryAsString(value: any): string {
return value.toString()
}
/// Main entry point. Loads WASM, initializes it, chooses the scene to run.
API.main = async function (inputConfig) {
let defaultConfig = {
use_loader : true,
wasm_url : '/assets/ide.wasm',
wasm_glue_url : '/assets/wasm_imports.js',
crash_report_host : cfg.defaultLogServerHost,
no_data_gathering : false,
is_in_cloud : false,
}
let urlParams = new URLSearchParams(window.location.search);
let urlConfig = Object.fromEntries(urlParams.entries())
let config = Object.assign(defaultConfig,inputConfig,urlConfig)
parseAllBooleans(config)
API.main = async function (inputConfig: any) {
const urlParams = new URLSearchParams(window.location.search)
// @ts-ignore
const urlConfig = Object.fromEntries(urlParams.entries())
const config = Config.default()
config.updateFromObject(inputConfig)
config.updateFromObject(urlConfig)
// @ts-ignore
API[globalConfig.windowAppScopeConfigName] = config
initLogging(config)
API.initLogging(config)
window.setInterval(() =>{API.remoteLog("alive");}, ALIVE_LOG_INTERVAL)
//Build data injected during the build process. See `webpack.config.js` for the source.
API.remoteLog("git_hash", {hash: GIT_HASH})
API.remoteLog("build_information", BUILD_INFO)
API.remoteLog("git_status", {satus: GIT_STATUS})
window.setInterval(() => {
API.remoteLog('alive')
}, ALIVE_LOG_INTERVAL)
// Build data injected during the build process. See `webpack.config.js` for the source.
// @ts-ignore
const hash = GIT_HASH
API.remoteLog('git_hash', { hash })
// @ts-ignore
const buildInfo = BUILD_INFO
API.remoteLog('build_information', buildInfo)
// @ts-ignore
const status = GIT_STATUS
API.remoteLog('git_status', { status })
//initCrashHandling()
style_root()
@ -500,21 +577,23 @@ API.main = async function (inputConfig) {
disableContextMenu()
let entryTarget = ok(config.entry) ? config.entry : main_entry_point
config.use_loader = config.use_loader && (entryTarget === main_entry_point)
config.use_loader = config.use_loader && entryTarget === main_entry_point
API.remoteLog("window_show_animation")
API.remoteLog('window_show_animation')
await windowShowAnimation()
API.remoteLog("download_content")
let {wasm,loader} = await download_content(config)
API.remoteLog("wasm_loaded")
API.remoteLog('download_content')
let { wasm, loader } = await download_content(config)
API.remoteLog('wasm_loaded')
if (entryTarget) {
let fn_name = wasm_entry_point_pfx + entryTarget
let fn = wasm[fn_name]
if (fn) { fn() } else {
let fn = wasm[fn_name]
if (fn) {
fn()
} else {
loader.destroy()
show_debug_screen(wasm,"Unknown entry point '" + entryTarget + "'. ")
show_debug_screen(wasm, "Unknown entry point '" + entryTarget + "'. ")
}
} else {
show_debug_screen(wasm)
show_debug_screen(wasm, '')
}
}

View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"noImplicitAny": true,
"target": "ES5",
"module": "ES2015"
}
}

View File

@ -18,7 +18,7 @@ const BUILD_INFO = JSON.parse(require('fs').readFileSync(buildPath, 'utf8'));
module.exports = {
entry: {
index: path.resolve(thisPath,'src','index.js'),
index: path.resolve(thisPath,'src','index.ts'),
wasm_imports: './src/wasm_imports.js',
},
output: {
@ -51,8 +51,9 @@ module.exports = {
},
resolve: {
alias: {
wasm_rust_glue$: path.resolve(wasmPath,'ide.js')
}
wasm_rust_glue$: path.resolve(wasmPath,'ide.js'),
},
extensions: [ '.ts', '.js' ],
},
performance: {
hints: false,
@ -65,6 +66,11 @@ module.exports = {
test: /\.ya?ml$/,
type: 'json',
use: 'yaml-loader'
},
{
test: /\.tsx?/,
use: 'ts-loader',
exclude: /node_modules/,
}
]
}