in runner/runners/local_output.go [88:256]
func (s *localOutputCreator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
clientHtml :=
`<html>
<script type="text/javascript">
// This code makes use of ES6+ constructs, such as
// arrow functions: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
// async await: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await
// promise: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
// typed arrays: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays
let resourceId = "";
const oneHour = 60 * 60 * 1000;
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const checkAtBottom = () => {
//scrolling: http://stackoverflow.com/a/22394544
let scrollTop =
(document.documentElement && document.documentElement.scrollTop) ||
document.body.scrollTop;
let scrollHeight =
(document.documentElement && document.documentElement.scrollHeight) ||
document.body.scrollHeight;
return scrollTop + window.innerHeight >= scrollHeight;
};
const gotoBottom = () => {
let scrollHeight =
(document.documentElement && document.documentElement.scrollHeight) ||
document.body.scrollHeight;
let scrollLeft =
(document.documentElement && document.documentElement.scrollLeft) ||
document.body.scrollLeft;
window.scrollTo(scrollLeft, scrollHeight);
};
// parseLength parses the Content-Range http header for the size of the resource
const parseLength = resp => {
let contentRange = resp.headers.get("Content-Range");
let idx = contentRange.lastIndexOf("/") + 1;
return contentRange.slice(idx);
};
const copyBuffer = (oldBuffer, length) => {
buffer = new Uint8Array(length);
buffer.set(oldBuffer);
return {
buffer,
length
};
};
const writeBuffer = (mainBuffer, arrayBuffer, offset) => {
let bytes = new Uint8Array(arrayBuffer);
for (let i = 0; i < bytes.byteLength; i++) {
mainBuffer[i + offset] = bytes[i];
}
};
const updateText = buffer => {
let wasAtBottom = checkAtBottom();
let div = document.getElementById("output");
div.innerText = new TextDecoder("utf-8").decode(buffer);
if (wasAtBottom) {
gotoBottom();
}
};
const increaseTimeout = currTimeout => Math.trunc((currTimeout * 3) / 2);
sendRequest = async () => {
let url =
location.href + (location.search == "" ? "?" : "&") + "content=true";
let resp = await fetch(url, {
method: "HEAD"
});
let length = Number.parseInt(resp.headers.get("Content-Length"));
// buffer is an Uint8Array to preserve utf-8 encoding
let buffer = new Uint8Array(length);
let curr = 0;
// 1280KB was chosen as it would be larger than most small files, and
// for larger files, say around 20MB, would be able to be retrieved in
// approximately 20 calls.
// 1280KB == 1310720 bytes
let offset = 1310720;
let minTimeout = 50;
let currTimeout = minTimeout;
// 15 minutes
let maxTimeout = 15 * 60 * 1000;
// Read 1280KB chunks until the entire resource is consumed, and wait for updates
// up to one hour after last modified date
while (true) {
let next = Math.min(curr + offset, length);
// if curr == next then we have reached the end of our file
// and need to wait for updates to be written
if (curr == next) {
let resp = await fetch(url, {
method: "HEAD"
});
length = Number.parseInt(resp.headers.get("Content-Length"));
// Date.parse and Date.now returns epoch time
lastModified = Date.parse(resp.headers.get("Last-Modified"));
if (Date.now() - lastModified > oneHour) {
// log hasn't been update in over 1 hour
// so stop fetching
break;
}
currTimeout = increaseTimeout(currTimeout);
} else {
// Make an HTTP Range Request
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
resp = await fetch(url, {
headers: new Headers({
Range: "bytes=" + curr + "-" + next
})
});
if (200 <= resp.status && resp.status < 300) {
let id = resp.headers.get("X-Resource-Id");
let newLength = parseLength(resp);
// if newLength != length that means the file is still being written to
// so increase the time between retries
if (newLength != length) {
minTimeout = 5000;
({ buffer, length } = copyBuffer(buffer, newLength));
}
writeBuffer(buffer, await resp.arrayBuffer(), curr);
updateText(buffer);
if (resourceId == "") {
resourceId = id;
}
if (id != resourceId) {
alert("Underlying resource changed! Quitting");
break;
}
currTimeout = minTimeout;
curr = next;
} else {
currTimeout = increaseTimeout(currTimeout);
}
}
await sleep(Math.min(currTimeout, maxTimeout));
}
};
sendRequest();
</script>
<body>
<div id="output" style="white-space: pre-wrap"></div>
</body>
</html>
`
if strings.TrimSuffix(r.URL.Path, "/")+"/" == s.HttpPath() {
http.StripPrefix(s.HttpPath(), http.FileServer(http.Dir(s.tmp))).ServeHTTP(w, r)
return
}
path := strings.TrimPrefix(r.URL.Path, s.HttpPath())
filepath, ok := s.pathMap[path]
if !ok {
http.Error(w, "Unrecognized path", http.StatusNotFound)
} else if resource, err := os.Open(filepath); err != nil {
http.Error(w, "", http.StatusGone)
} else if info, err := resource.Stat(); err != nil {
http.Error(w, "", http.StatusInternalServerError)
} else {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("X-Resource-Id", filepath)
if r.URL.Query().Get("content") == "true" {
w.Header().Set("Cache-Control", "no-store")
http.ServeContent(w, r, "", info.ModTime(), resource)
} else {
fmt.Fprintf(w, clientHtml)
}
}
}