2023-02-06 01:53:34 +08:00
|
|
|
import {set_record_call} from './runtime.js'
|
2023-02-07 20:09:43 +08:00
|
|
|
|
2024-02-14 13:36:51 +08:00
|
|
|
const io_patch = (window, path, use_context = false) => {
|
|
|
|
|
let obj = window
|
2023-02-07 20:09:43 +08:00
|
|
|
for(let i = 0; i < path.length - 1; i++) {
|
|
|
|
|
obj = obj[path[i]]
|
|
|
|
|
}
|
|
|
|
|
const method = path.at(-1)
|
2023-02-06 01:53:34 +08:00
|
|
|
if(obj == null || obj[method] == null) {
|
|
|
|
|
// Method is absent in current env, skip patching
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-02-07 20:09:43 +08:00
|
|
|
const name = path.join('.')
|
|
|
|
|
|
2023-02-06 01:53:34 +08:00
|
|
|
const original = obj[method]
|
2023-06-26 17:28:23 +03:00
|
|
|
|
2024-02-14 13:36:51 +08:00
|
|
|
obj[method] = make_patched_method(window, original, name, use_context)
|
2023-06-26 17:28:23 +03:00
|
|
|
|
|
|
|
|
obj[method].__original = original
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-14 13:36:51 +08:00
|
|
|
const make_patched_method = (window, original, name, use_context) => {
|
2023-06-26 17:28:23 +03:00
|
|
|
const method = function(...args) {
|
2024-02-14 13:36:51 +08:00
|
|
|
|
|
|
|
|
const cxt = window.__cxt
|
|
|
|
|
|
2023-06-27 15:03:03 +03:00
|
|
|
if(cxt.io_trace_is_replay_aborted) {
|
2023-02-06 01:53:34 +08:00
|
|
|
// Try to finish fast
|
2023-07-14 05:40:13 +03:00
|
|
|
const error = new Error('io replay was aborted')
|
|
|
|
|
error.__ignore = true
|
|
|
|
|
throw error
|
2023-02-07 23:07:37 +08:00
|
|
|
}
|
|
|
|
|
|
2023-02-14 18:03:10 +08:00
|
|
|
const has_new_target = new.target != null
|
|
|
|
|
|
|
|
|
|
if(cxt.is_recording_deferred_calls) {
|
2023-06-27 15:03:03 +03:00
|
|
|
// TODO record trace on deferred calls?
|
2023-02-14 18:03:10 +08:00
|
|
|
return has_new_target
|
|
|
|
|
? new original(...args)
|
|
|
|
|
: original.apply(this, args)
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-17 16:55:13 +03:00
|
|
|
const cxt_copy = cxt
|
|
|
|
|
|
2023-06-27 15:03:03 +03:00
|
|
|
if(cxt.io_trace_is_recording) {
|
2023-02-06 01:53:34 +08:00
|
|
|
let ok, value, error
|
|
|
|
|
try {
|
2023-02-14 18:03:10 +08:00
|
|
|
// save call, so on expand_call and find_call IO functions would not be
|
|
|
|
|
// called.
|
|
|
|
|
// TODO: we have a problem when IO function is called from third-party
|
|
|
|
|
// lib and async context is lost
|
2023-02-06 01:53:34 +08:00
|
|
|
set_record_call(cxt)
|
|
|
|
|
|
2023-06-27 15:03:03 +03:00
|
|
|
const index = cxt.io_trace.length
|
2023-02-06 01:53:34 +08:00
|
|
|
|
|
|
|
|
if(name == 'setTimeout') {
|
|
|
|
|
args = args.slice()
|
|
|
|
|
// Patch callback
|
|
|
|
|
const cb = args[0]
|
2023-02-14 18:03:10 +08:00
|
|
|
args[0] = Object.defineProperty(function() {
|
2023-05-17 16:55:13 +03:00
|
|
|
if(cxt_copy != cxt) {
|
2023-05-13 11:13:34 +03:00
|
|
|
// If code execution was cancelled, then never call callback
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-06-27 15:03:03 +03:00
|
|
|
if(cxt.io_trace_is_replay_aborted) {
|
2023-02-14 18:03:10 +08:00
|
|
|
// Non necessary
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-06-27 15:03:03 +03:00
|
|
|
cxt.io_trace.push({type: 'resolution', index})
|
2023-02-06 01:53:34 +08:00
|
|
|
cb()
|
2023-02-14 18:03:10 +08:00
|
|
|
}, 'name', {value: cb.name})
|
2023-02-06 01:53:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
value = has_new_target
|
|
|
|
|
? new original(...args)
|
|
|
|
|
: original.apply(this, args)
|
|
|
|
|
|
2024-02-15 17:03:26 +08:00
|
|
|
if(value?.[Symbol.toStringTag] == 'Promise') {
|
2023-06-05 15:53:08 +03:00
|
|
|
// TODO use __original_then, not finally which calls
|
2023-02-08 05:32:32 +08:00
|
|
|
// patched 'then'?
|
|
|
|
|
value = value.finally(() => {
|
2023-05-17 16:55:13 +03:00
|
|
|
if(cxt_copy != cxt) {
|
2023-05-13 11:13:34 +03:00
|
|
|
return
|
|
|
|
|
}
|
2023-06-27 15:03:03 +03:00
|
|
|
if(cxt.io_trace_is_replay_aborted) {
|
2023-02-14 18:03:10 +08:00
|
|
|
// Non necessary
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-06-27 15:03:03 +03:00
|
|
|
cxt.io_trace.push({type: 'resolution', index})
|
2023-02-06 01:53:34 +08:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ok = true
|
|
|
|
|
return value
|
|
|
|
|
} catch(e) {
|
|
|
|
|
error = e
|
|
|
|
|
ok = false
|
|
|
|
|
throw e
|
|
|
|
|
} finally {
|
2023-06-27 15:03:03 +03:00
|
|
|
cxt.io_trace.push({
|
2023-02-06 01:53:34 +08:00
|
|
|
type: 'call',
|
|
|
|
|
name,
|
|
|
|
|
ok,
|
|
|
|
|
value,
|
|
|
|
|
error,
|
|
|
|
|
args,
|
|
|
|
|
// To discern calls with and without 'new' keyword, primary for
|
|
|
|
|
// Date that can be called with and without new
|
|
|
|
|
has_new_target,
|
|
|
|
|
use_context,
|
|
|
|
|
context: use_context ? this : undefined,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2023-07-06 21:23:06 +03:00
|
|
|
// IO trace replay
|
|
|
|
|
|
2023-06-27 15:03:03 +03:00
|
|
|
const call = cxt.io_trace[cxt.io_trace_index]
|
2023-02-07 21:36:36 +08:00
|
|
|
|
2023-02-14 18:03:10 +08:00
|
|
|
// TODO if call == null or call.type == 'resolution', then do not discard
|
2023-06-27 15:03:03 +03:00
|
|
|
// trace, instead switch to record mode and append new calls to the
|
|
|
|
|
// trace?
|
2023-02-06 01:53:34 +08:00
|
|
|
if(
|
|
|
|
|
call == null
|
|
|
|
|
|| call.type != 'call'
|
2023-02-14 18:03:10 +08:00
|
|
|
|| call.has_new_target != has_new_target
|
2023-02-06 01:53:34 +08:00
|
|
|
|| call.use_context && (call.context != this)
|
|
|
|
|
|| call.name != name
|
|
|
|
|
|| (
|
2023-02-14 18:03:10 +08:00
|
|
|
(name == 'setTimeout' && (args[1] != call.args[1])) /* compares timeout*/
|
|
|
|
|
||
|
|
|
|
|
(
|
|
|
|
|
name != 'setTimeout'
|
|
|
|
|
&&
|
|
|
|
|
JSON.stringify(call.args) != JSON.stringify(args)
|
|
|
|
|
)
|
2023-02-06 01:53:34 +08:00
|
|
|
)
|
|
|
|
|
){
|
2023-06-27 15:03:03 +03:00
|
|
|
cxt.io_trace_is_replay_aborted = true
|
2023-07-15 17:59:58 +03:00
|
|
|
cxt.io_trace_abort_replay()
|
|
|
|
|
// throw error to prevent further code execution. It
|
|
|
|
|
// is not necesseary, becuase execution would not have
|
|
|
|
|
// any effects anyway
|
2023-07-14 05:40:13 +03:00
|
|
|
const error = new Error('io replay aborted')
|
|
|
|
|
error.__ignore = true
|
|
|
|
|
throw error
|
2023-02-06 01:53:34 +08:00
|
|
|
} else {
|
2023-02-14 18:03:10 +08:00
|
|
|
|
2023-06-27 15:03:03 +03:00
|
|
|
const next_resolution = cxt.io_trace.find((e, i) =>
|
|
|
|
|
e.type == 'resolution' && i > cxt.io_trace_index
|
2023-02-06 01:53:34 +08:00
|
|
|
)
|
|
|
|
|
|
2023-06-27 15:03:03 +03:00
|
|
|
if(next_resolution != null && !cxt.io_trace_resolver_is_set) {
|
2023-02-07 20:09:43 +08:00
|
|
|
const original_setTimeout = cxt.window.setTimeout.__original
|
2023-06-27 15:03:03 +03:00
|
|
|
cxt.io_trace_resolver_is_set = true
|
2023-02-06 01:53:34 +08:00
|
|
|
|
|
|
|
|
original_setTimeout(() => {
|
2023-05-17 16:55:13 +03:00
|
|
|
if(cxt_copy != cxt) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-02-14 18:03:10 +08:00
|
|
|
|
2023-06-27 15:03:03 +03:00
|
|
|
if(cxt.io_trace_is_replay_aborted) {
|
2023-02-07 23:07:37 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-27 15:03:03 +03:00
|
|
|
cxt.io_trace_resolver_is_set = false
|
2023-02-06 01:53:34 +08:00
|
|
|
|
2023-02-07 23:07:37 +08:00
|
|
|
// Sanity check
|
2023-06-27 15:03:03 +03:00
|
|
|
if(cxt.io_trace_index >= cxt.io_trace.length) {
|
2023-02-06 01:53:34 +08:00
|
|
|
throw new Error('illegal state')
|
2023-02-07 23:07:37 +08:00
|
|
|
}
|
|
|
|
|
|
2023-06-27 15:03:03 +03:00
|
|
|
const next_event = cxt.io_trace[cxt.io_trace_index]
|
2023-02-07 23:07:37 +08:00
|
|
|
if(next_event.type == 'call') {
|
2023-06-27 15:03:03 +03:00
|
|
|
cxt.io_trace_is_replay_aborted = true
|
2023-07-15 17:59:58 +03:00
|
|
|
cxt.io_trace_abort_replay()
|
2023-02-06 01:53:34 +08:00
|
|
|
} else {
|
2023-02-07 23:07:37 +08:00
|
|
|
while(
|
2023-06-27 15:03:03 +03:00
|
|
|
cxt.io_trace_index < cxt.io_trace.length
|
2023-02-07 23:07:37 +08:00
|
|
|
&&
|
2023-06-27 15:03:03 +03:00
|
|
|
cxt.io_trace[cxt.io_trace_index].type == 'resolution'
|
2023-02-07 23:07:37 +08:00
|
|
|
) {
|
2023-06-27 15:03:03 +03:00
|
|
|
const resolution = cxt.io_trace[cxt.io_trace_index]
|
|
|
|
|
const resolver = cxt.io_trace_resolvers.get(resolution.index)
|
2023-02-07 23:07:37 +08:00
|
|
|
|
2023-06-27 15:03:03 +03:00
|
|
|
cxt.io_trace_index++
|
2023-02-07 23:07:37 +08:00
|
|
|
|
2023-06-27 15:03:03 +03:00
|
|
|
if(cxt.io_trace[resolution.index].name == 'setTimeout') {
|
2023-02-07 23:07:37 +08:00
|
|
|
resolver()
|
|
|
|
|
} else {
|
2023-06-27 15:03:03 +03:00
|
|
|
resolver(cxt.io_trace[resolution.index].value)
|
2023-02-06 01:53:34 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}, 0)
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-27 15:03:03 +03:00
|
|
|
cxt.io_trace_index++
|
2023-02-06 01:53:34 +08:00
|
|
|
|
|
|
|
|
if(call.ok) {
|
2023-07-06 21:23:06 +03:00
|
|
|
// Use Symbol.toStringTag for comparison because Promise may
|
|
|
|
|
// originate from another window (if window was reopened after record
|
|
|
|
|
// trace) and instanceof would not work
|
|
|
|
|
if(call.value?.[Symbol.toStringTag] == 'Promise') {
|
2023-07-11 18:24:28 +03:00
|
|
|
// Always make promise originate from app_window
|
2023-02-08 05:32:32 +08:00
|
|
|
return new cxt.window.Promise(resolve => {
|
2023-06-27 15:03:03 +03:00
|
|
|
cxt.io_trace_resolvers.set(cxt.io_trace_index - 1, resolve)
|
2023-02-06 01:53:34 +08:00
|
|
|
})
|
|
|
|
|
} else if(name == 'setTimeout') {
|
|
|
|
|
const timeout_cb = args[0]
|
2023-06-27 15:03:03 +03:00
|
|
|
cxt.io_trace_resolvers.set(cxt.io_trace_index - 1, timeout_cb)
|
2023-02-06 01:53:34 +08:00
|
|
|
return call.value
|
|
|
|
|
} else {
|
|
|
|
|
return call.value
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
throw call.error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-26 17:28:23 +03:00
|
|
|
Object.defineProperty(method, 'name', {value: original.name})
|
2023-02-06 01:53:34 +08:00
|
|
|
|
2023-06-26 17:28:23 +03:00
|
|
|
return method
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-14 13:36:51 +08:00
|
|
|
const patch_Date = (window) => {
|
|
|
|
|
const Date = window.Date
|
|
|
|
|
const Date_patched = make_patched_method(window, Date, 'Date', false)
|
|
|
|
|
window.Date = function(...args) {
|
2023-06-26 17:28:23 +03:00
|
|
|
if(args.length == 0) {
|
|
|
|
|
// return current Date, IO operation
|
|
|
|
|
if(new.target != null) {
|
|
|
|
|
return new Date_patched(...args)
|
|
|
|
|
} else {
|
|
|
|
|
return Date_patched(...args)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// pure function
|
|
|
|
|
if(new.target != null) {
|
|
|
|
|
return new Date(...args)
|
|
|
|
|
} else {
|
|
|
|
|
return Date(...args)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-14 13:36:51 +08:00
|
|
|
window.Date.__original = Date
|
2023-06-26 17:28:23 +03:00
|
|
|
|
2024-02-14 13:36:51 +08:00
|
|
|
window.Date.parse = Date.parse
|
|
|
|
|
window.Date.now = Date.now
|
|
|
|
|
window.Date.UTC = Date.UTC
|
|
|
|
|
io_patch(window, ['Date', 'now'])
|
2023-02-06 01:53:34 +08:00
|
|
|
}
|
|
|
|
|
|
2024-02-15 19:46:05 +08:00
|
|
|
export const apply_io_patches = (window) => {
|
2024-02-14 13:36:51 +08:00
|
|
|
io_patch(window, ['Math', 'random'])
|
2023-02-06 01:53:34 +08:00
|
|
|
|
2024-02-14 13:36:51 +08:00
|
|
|
io_patch(window, ['setTimeout'])
|
2023-06-27 15:03:03 +03:00
|
|
|
// TODO if call setTimeout and then clearTimeout, trace it and remove call of
|
2023-02-14 18:03:10 +08:00
|
|
|
// clearTimeout, and make only setTimeout, then it would never be called when
|
2023-06-27 15:03:03 +03:00
|
|
|
// replaying from trace
|
2024-02-14 13:36:51 +08:00
|
|
|
io_patch(window, ['clearTimeout'])
|
2023-02-06 01:53:34 +08:00
|
|
|
|
2023-02-14 18:03:10 +08:00
|
|
|
// TODO patch setInterval to only cleanup all intervals on finish
|
|
|
|
|
|
2024-02-14 13:36:51 +08:00
|
|
|
patch_Date(window)
|
2023-02-06 01:53:34 +08:00
|
|
|
|
2024-02-14 13:36:51 +08:00
|
|
|
io_patch(window, ['fetch'])
|
2023-02-06 01:53:34 +08:00
|
|
|
// Check if Response is defined, for node.js
|
2024-02-15 19:46:05 +08:00
|
|
|
if(window.Response != null) {
|
2023-05-13 11:13:29 +03:00
|
|
|
const Response_methods = [
|
|
|
|
|
'arrayBuffer',
|
|
|
|
|
'blob',
|
|
|
|
|
'formData',
|
|
|
|
|
'json',
|
|
|
|
|
'text',
|
|
|
|
|
]
|
2023-02-06 01:53:34 +08:00
|
|
|
for(let key of Response_methods) {
|
2024-02-14 13:36:51 +08:00
|
|
|
io_patch(window, ['Response', 'prototype', key], true)
|
2023-02-06 01:53:34 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|