CORE/src/erm.gd
JeremyStarTM acc305d3db
Update module logging and update many log calls
This commit firstly removes the 'logger' variable in CoreBaseModule, secondly renames 'loggeri' to 'logger' in CoreBaseModule, effectively replacing it, and thirdly it forces using 'stringify_variables' onto all log calls.
2024-04-25 20:20:34 +02:00

184 lines
10 KiB
GDScript

# CORE FRAMEWORK SOURCE FILE
# Copyright (c) 2024 The StarOpenSource Project & Contributors
# Licensed under the GNU Affero General Public License v3
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
## Allows for awaited, batched and oneline requests.
extends CoreBaseModule
# Contains a list of all queued downloads.
var list_queue: Dictionary = {}
# Contains a list of all active downloads.
var list_active: Dictionary = {}
# Contains a liust of all completed downloads.
var list_complete: Dictionary = {}
## Determines how unsecure requests should be handled.
var config_unsecure_requests: CoreTypes.BlockadeLevel
# +++ module +++
func _cleanup() -> void:
clean_queue()
clean_completed()
for child in get_children():
if child.is_class("HTTPRequest"):
child.cancel_request()
await get_tree().process_frame
remove_child(child)
# +++ methods that do the heavily lifting +++
## Requests a file from the internet.[br]
## [br]
## The returned [code]Dictionary[/code] has the following structure (example):
## [codeblock]
## { "result": 0, "http_code": 200, "headers": [ "Server": "nginx" ], "body": [], "body_utf8": [] }
## ^ ^ ^ ^ ^
## | | | | |
## | | | | --------- The body from the server, as a UTF-8 string (set to "" if 'parse_utf8' is false)
## | | | ----------------------- The body from the server, as bytes
## | | ------------------------------------------------------ A array of headers
## | ---------------------------------------------------------------------- The HTTP response code
## ------------------------------------------------------------------------------------ Equal to @GlobalScope.Error. If not 0/Error.OK = the request failed
## [/codeblock]
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b]
func awaited_request(url: String, parse_utf8: bool, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> Dictionary:
logger.verb("Creating awaited request")
if !await is_url_allowed(url): return {}
var id: int = create_request(url, method, headers, data)
start_request(id, parse_utf8)
logger.diag("Waiting for request " + str(id) + " to finish")
logger.diag(core.stringify_variables("Waiting for request %id% to finish", { "id": id }))
while !is_request_completed(id): await get_tree().create_timer(0.1, true).timeout
var dldata: Dictionary = list_complete[id]
list_complete.erase(id)
return dldata
## Requests a file from the internet without returning the godot code, http code or headers. Useful for oneliners.[br]
## [br]
## Returns [code]null[/code] on error. To ignore HTTP errors (ie. non-200 statuses) set [code]ignore_http_code[/code] to [code]true[/code].[br]
## Returns a UTF-8 string with [code]return_utf8[/code] turned on, returns bytes when turned off.[br]
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b]
func oneline_awaited_request(url: String, return_utf8: bool = true, ignore_http_code: bool = false, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> Variant:
var dldata: Dictionary = await awaited_request(url, return_utf8, method, headers, data)
if dldata == {}: return null
if dldata["result"] != Error.OK: return null
elif !ignore_http_code and dldata["http_code"] != 200: return null
else:
if return_utf8: return dldata["body_utf8"]
else: return dldata["body"]
## Requests multiple files from the internet.[br]
## [br]
## Thee returned [code]Dictionary[/code]s have the following structure (example):
## [codeblock]
## { "result": 0, "http_code": 200, "headers": [ "Server": "nginx" ], "body": [], "body_utf8": [] }
## ^ ^ ^ ^ ^
## | | | | |
## | | | | --------- The body from the server, as a UTF-8 string (set to "" if 'parse_utf8' is false)
## | | | ----------------------- The body from the server, as bytes
## | | ------------------------------------------------------ A array of headers
## | ---------------------------------------------------------------------- The HTTP response code
## ------------------------------------------------------------------------------------ Equal to @GlobalScope.Error. If not 0/Error.OK = the request failed
## [/codeblock]
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b]
func batch_awaited_request(urls: PackedStringArray, parse_utf8: bool, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> Array[Dictionary]:
logger.verb("Creating " + str(urls.size()) + " awaited request(s)")
var dldata: Array[Dictionary] = []
for url in urls:
if !await is_url_allowed(url): continue
var thread: Thread = Thread.new()
thread.start(Callable(self, "_batch_awaited_request").bind(url, parse_utf8, method, headers, data))
var id: int = thread.wait_to_finish()
dldata.append(list_complete[id])
list_complete.erase(id)
return dldata
# Does the work, but in a thread.
func _batch_awaited_request(url: String, parse_utf8: bool, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> int:
var id: int = create_request(url, method, headers, data)
start_request(id, parse_utf8)
logger.diag(core.stringify_variables("Waiting for request %id% to finish", { "id": id }))
while !is_request_completed(id): await get_tree().create_timer(0.1, true).timeout
return id
# +++ internal +++
# Returns a new download id.
func generate_id() -> int:
var id = randi()
if list_queue.has(id) or list_active.has(id):
logger.warn(core.stringify_variables("New download id %id% already taken", { "id": id }))
return generate_id()
logger.diag(core.stringify_variables("Generated new download id %id%", { "id": id }))
return id
## Creates a new request and stores it in the queue. Returns the download id.[br]
## [b]Warning: [i]You'll probably not need this. Only use this function when implementing your own downloading method.[/i][/b]
func create_request(url: String, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), body: String = "") -> int:
logger.verb("Creating new request\n-> URL: " + url + "\n-> Method: " + str(method) + "\nHeaders: " + str(headers.size()) + "\nBody size: " + str(body.length()) + " Characters")
var id = generate_id()
list_queue.merge({ id: { "url": url, "method": method, "headers": headers, "body": body } })
return id
## Configures and starts a queued request.[br]
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b][br]
## [b]Warning: [i]You'll probably not need this. Only use this function when implementing your own downloading method.[/i][/b]
func start_request(id: int, parse_utf8: bool) -> void:
logger.verb("Starting request " + str(id))
logger.verb(core.stringify_variables("Starting request %id%", { "id": id }))
list_active.merge({ id: list_queue[id] })
list_queue.erase(id)
logger.diag("Creating new HTTPRequest \"Request #" + str(id) + "\"")
var download: HTTPRequest = HTTPRequest.new()
download.name = "Request #" + str(id)
download.accept_gzip = true
download.use_threads = true
var lambda: Callable = func(result: int, http_code: int, headers: PackedStringArray, body: PackedByteArray, httprequest: HTTPRequest) -> void:
logger.verb(core.stringify_variables("Request %id% completed\nResult: %result%\nHTTP response code: %http_code%\nHeaders: %headers%\nBody size: %body_size_bytes% Bytes // %body_size_mib% MiB\nParsed body as UTF-8: %parse_utf8%", { "result": result, "http_code": http_code, "headers": headers.size(), "body_size_bytes": body.size(), "body_size_mib": core.misc.byte2mib(body.size(), true), "parse_utf8": parse_utf8 }, true))
list_complete.merge({ id: { "result": result, "http_code": http_code, "headers": headers, "body": body, "body_utf8": body.get_string_from_utf8() if parse_utf8 else "" } })
list_active.erase(id)
remove_child(httprequest)
httprequest.queue_free()
download.connect("request_completed", lambda.bind(download))
add_child(download)
download.request(list_active[id]["url"], list_active[id]["headers"], list_active[id]["method"], list_active[id]["body"])
## Checks if [code]url[/code] can be used.
func is_url_allowed(url: String) -> bool:
if url.begins_with("http://"):
match(config_unsecure_requests):
CoreTypes.BlockadeLevel.BLOCK:
logger.error(core.stringify_variables("Blocked unsecure url %url%", { "url": url }))
return false
CoreTypes.BlockadeLevel.WARN: logger.warn(core.stringify_variables("Requesting unsecure url %url%", { "url": url }))
CoreTypes.BlockadeLevel.IGNORE: pass
_: await logger.crash(core.stringify_variables("Invalid BlockadeLevel %level%", { "level": config_unsecure_requests }))
elif url.begins_with("https://"): pass
else:
logger.error(core.stringify_variables("Invalid url %url%", { "url": url }))
return false
return true
## Returns if a request has completed yet.
func is_request_completed(id: int) -> bool: return list_complete.has(id)
## Cleans the request queue.
func clean_queue() -> void:
logger.verb("Cleaning request queue")
list_queue.clear()
## Cleans the completed requests list.
func clean_completed() -> void:
logger.verb("Cleaning completed requests")
list_complete.clear()