From 9d978fd9ab53b4c7e7864251a1081251b0f4f473 Mon Sep 17 00:00:00 2001 From: Alessandro Proto Date: Sun, 8 Jan 2023 18:41:17 +0100 Subject: [PATCH] Added http.request to Lua, renamed Lua.PushValue to Lua.PushObject, added lua.lua REPL --- .gitignore | 3 +- Capy64/API/IPlugin.cs | 2 +- Capy64/Assets/Lua/bin/lua.lua | 78 ++++++++++ Capy64/Assets/Lua/boot/99_shell.lua | 2 +- Capy64/BIOS/Bios.cs | 2 +- Capy64/Capy64.cs | 10 +- Capy64/IGame.cs | 1 + Capy64/LuaRuntime/Extensions/Utils.cs | 43 +++--- Capy64/LuaRuntime/Libraries/FileSystem.cs | 5 +- Capy64/LuaRuntime/Libraries/HTTP.cs | 169 +++++++++++++++++++++- Capy64/LuaRuntime/Libraries/OS.cs | 2 +- Capy64/LuaRuntime/Runtime.cs | 2 +- 12 files changed, 282 insertions(+), 37 deletions(-) create mode 100644 Capy64/Assets/Lua/bin/lua.lua diff --git a/.gitignore b/.gitignore index 9491a2f..fda2c7d 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,8 @@ x86/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ -[Bb]in/ +/[Bb]in/ +/[Cc]apy64/bin/ [Oo]bj/ [Oo]ut/ [Ll]og/ diff --git a/Capy64/API/IPlugin.cs b/Capy64/API/IPlugin.cs index 941b54c..94193d5 100644 --- a/Capy64/API/IPlugin.cs +++ b/Capy64/API/IPlugin.cs @@ -11,6 +11,6 @@ namespace Capy64.API; public interface IPlugin { void ConfigureServices(IServiceCollection services) { } - void LuaInit(Lua state) { } + void LuaInit(Lua L) { } } diff --git a/Capy64/Assets/Lua/bin/lua.lua b/Capy64/Assets/Lua/bin/lua.lua new file mode 100644 index 0000000..7b86518 --- /dev/null +++ b/Capy64/Assets/Lua/bin/lua.lua @@ -0,0 +1,78 @@ +local term = require("term") +local io = require("io") +local colors = require("colors") +local colours = colors + +local tArgs = { ... } +if #tArgs > 0 then + print("This is an interactive Lua prompt.") + print("To run a lua program, just type its name.") + return +end + +--local pretty = require "cc.pretty" + +local bRunning = true +local tCommandHistory = {} +local tEnv = { + ["exit"] = setmetatable({}, { + __tostring = function() return "Call exit() to exit." end, + __call = function() bRunning = false end, + }), + ["_echo"] = function(...) + return ... + end, +} +setmetatable(tEnv, { __index = _ENV }) + +for k, v in pairs(package.loaded) do + tEnv[k] = v +end + +term.setForeground(colours.yellow) +print("Interactive Lua prompt.") +print("Call exit() to exit.") +term.setForeground(colours.white) + +while bRunning do + term.setForeground( colours.yellow ) + write("lua> ") + term.setForeground( colours.white ) + + local s = io.read(nil, tCommandHistory) + if s:match("%S") and tCommandHistory[#tCommandHistory] ~= s then + table.insert(tCommandHistory, s) + end + + local nForcePrint = 0 + local func, e = load(s, "=lua", "t", tEnv) + local func2 = load("return _echo(" .. s .. ");", "=lua", "t", tEnv) + if not func then + if func2 then + func = func2 + e = nil + nForcePrint = 1 + end + else + if func2 then + func = func2 + end + end + + if func then + local tResults = table.pack(pcall(func)) + if tResults[1] then + local n = 1 + while n < tResults.n or n <= nForcePrint do + local value = tResults[n + 1] + print(tostring(value)) + n = n + 1 + end + else + print(tResults[2]) + end + else + print(e) + end + +end \ No newline at end of file diff --git a/Capy64/Assets/Lua/boot/99_shell.lua b/Capy64/Assets/Lua/boot/99_shell.lua index 9ccfc7d..4156a43 100644 --- a/Capy64/Assets/Lua/boot/99_shell.lua +++ b/Capy64/Assets/Lua/boot/99_shell.lua @@ -10,4 +10,4 @@ term.setPos(1,1) print(os.version()) -dofile("/bin/shell.lua") \ No newline at end of file +dofile("/bin/lua.lua") \ No newline at end of file diff --git a/Capy64/BIOS/Bios.cs b/Capy64/BIOS/Bios.cs index a55232a..6d14143 100644 --- a/Capy64/BIOS/Bios.cs +++ b/Capy64/BIOS/Bios.cs @@ -144,7 +144,7 @@ public class Bios : IPlugin public static void InstallOS(bool force = false) { - var installedFilePath = Path.Combine(FileSystem.BasePath, ".installed"); + var installedFilePath = Path.Combine(Capy64.AppDataPath, ".installed"); if (!File.Exists(installedFilePath) || force) { FileSystem.CopyDirectory("Assets/Lua", FileSystem.DataPath, true, true); diff --git a/Capy64/Capy64.cs b/Capy64/Capy64.cs index c0d0049..9129f6d 100644 --- a/Capy64/Capy64.cs +++ b/Capy64/Capy64.cs @@ -15,12 +15,18 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using static Capy64.Utils; +using System.IO; namespace Capy64; public class Capy64 : Game, IGame { - public const string Version = "Capy64 a0.0.1"; + public const string Version = "a0.0.1"; + public static string AppDataPath = Path.Combine( + Environment.GetFolderPath( + Environment.SpecialFolder.ApplicationData, + Environment.SpecialFolderOption.Create), + "Capy64"); public Game Game => this; public IList NativePlugins { get; private set; } public IList Plugins { get; private set; } @@ -102,7 +108,7 @@ public class Capy64 : Game, IGame protected override void Initialize() { - Window.Title = Version; + Window.Title = "Capy64 " + Version; UpdateSize(); diff --git a/Capy64/IGame.cs b/Capy64/IGame.cs index a780813..0cbe8d8 100644 --- a/Capy64/IGame.cs +++ b/Capy64/IGame.cs @@ -10,6 +10,7 @@ namespace Capy64; public interface IGame { + Game Game { get; } IList NativePlugins { get; } IList Plugins { get; } diff --git a/Capy64/LuaRuntime/Extensions/Utils.cs b/Capy64/LuaRuntime/Extensions/Utils.cs index 93ae50b..612e92a 100644 --- a/Capy64/LuaRuntime/Extensions/Utils.cs +++ b/Capy64/LuaRuntime/Extensions/Utils.cs @@ -11,31 +11,31 @@ using System.Threading.Tasks; namespace Capy64.LuaRuntime.Extensions; public static class Utils { - public static void PushArray(this Lua state, object obj) + public static void PushArray(this Lua L, object obj) { var iterable = obj as IEnumerable; - state.NewTable(); + L.NewTable(); long i = 1; foreach (var item in iterable) { - state.PushValue(item); - state.RawSetInteger(-2, i++); + L.PushObject(item); + L.RawSetInteger(-2, i++); } - state.SetTop(-1); + L.SetTop(-1); } #nullable enable - public static int PushValue(this Lua state, object? obj) + public static int PushObject(this Lua L, object? obj) { var type = obj?.GetType(); switch (obj) { case string str: - state.PushString(str); + L.PushString(str); break; case char: - state.PushString(obj.ToString()); + L.PushString(obj.ToString()); break; case byte: @@ -45,37 +45,37 @@ public static class Utils case int: case uint: case double: - state.PushNumber(Convert.ToDouble(obj)); + L.PushNumber(Convert.ToDouble(obj)); break; case long l: - state.PushInteger(l); + L.PushInteger(l); break; case bool b: - state.PushBoolean(b); + L.PushBoolean(b); break; case null: - state.PushNil(); + L.PushNil(); break; case byte[] b: - state.PushBuffer(b); + L.PushBuffer(b); break; - + case LuaFunction func: - state.PushCFunction(func); + L.PushCFunction(func); break; case IntPtr ptr: - state.PushLightUserData(ptr); + L.PushLightUserData(ptr); break; default: if (type is not null && type.IsArray) { - state.PushArray(obj); + L.PushArray(obj); } else { @@ -87,15 +87,16 @@ public static class Utils return 1; } - public static void PushManagedObject(this Lua state, T obj) + [Obsolete("This method does not work as intended and requires more research")] + public static void PushManagedObject(this Lua L, T obj) { var type = obj.GetType(); var members = type.GetMembers().Where(m => m.MemberType == MemberTypes.Method); - state.CreateTable(0, members.Count()); + L.CreateTable(0, members.Count()); foreach (var m in members) { - state.PushCFunction(L => (int)type.InvokeMember(m.Name, BindingFlags.InvokeMethod, null, obj, new object[] { L })); - state.SetField(-2, m.Name); + L.PushCFunction(L => (int)type.InvokeMember(m.Name, BindingFlags.InvokeMethod, null, obj, new object[] { L })); + L.SetField(-2, m.Name); } } } diff --git a/Capy64/LuaRuntime/Libraries/FileSystem.cs b/Capy64/LuaRuntime/Libraries/FileSystem.cs index 9d60f5b..658824e 100644 --- a/Capy64/LuaRuntime/Libraries/FileSystem.cs +++ b/Capy64/LuaRuntime/Libraries/FileSystem.cs @@ -12,8 +12,7 @@ namespace Capy64.LuaRuntime.Libraries; public class FileSystem : IPlugin { - public static string BasePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "Capy64"); - public static string DataPath = Path.Combine(BasePath, "data"); + public static string DataPath = Path.Combine(Capy64.AppDataPath, "data"); public FileSystem() { @@ -451,7 +450,7 @@ public class FileSystem : IPlugin foreach (var attribute in attributes) { L.PushString(attribute.Key); - L.PushValue(attribute.Value); + Extensions.Utils.PushObject(L, attribute.Value); L.SetTable(-3); } diff --git a/Capy64/LuaRuntime/Libraries/HTTP.cs b/Capy64/LuaRuntime/Libraries/HTTP.cs index 3073bea..5583128 100644 --- a/Capy64/LuaRuntime/Libraries/HTTP.cs +++ b/Capy64/LuaRuntime/Libraries/HTTP.cs @@ -2,21 +2,180 @@ using KeraLua; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Net.Http; namespace Capy64.LuaRuntime.Libraries; - +#nullable enable public class HTTP : IPlugin { private static IGame _game; + private static HttpClient _client; + private static long RequestId; + + private readonly LuaRegister[] HttpLib = new LuaRegister[] + { + new() + { + name = "request", + function = L_Request, + }, + new() + { + name = "checkURL", + function = L_CheckUrl, + }, + new(), + }; public HTTP(IGame game) { _game = game; + RequestId = 0; + _client = new(); + _client.DefaultRequestHeaders.Add("User-Agent", $"Capy64/{Capy64.Version}"); } - public void LuaInit(Lua state) + public void LuaInit(Lua L) { + L.RequireF("http", Open, false); + } + + private int Open(IntPtr state) + { + var L = Lua.FromIntPtr(state); + L.NewLib(HttpLib); + return 1; + } + + public static bool TryGetUri(string url, out Uri? uri) + { + return (Uri.TryCreate(url, UriKind.Absolute, out uri) + && uri?.Scheme == Uri.UriSchemeHttp) || uri?.Scheme == Uri.UriSchemeHttps; + } + + private static int L_Request(IntPtr state) + { + var L = Lua.FromIntPtr(state); + var request = new HttpRequestMessage(); + + var url = L.CheckString(1); + if (!TryGetUri(url, out Uri? uri) || uri is null) + { + L.ArgumentError(1, "invalid request url"); + return 0; + } + request.RequestUri = uri; + + if (L.IsTable(3)) // headers + { + L.PushCopy(3); + L.PushNil(); + + while (L.Next(-2)) + { + L.PushCopy(-2); + + var k = L.CheckString(-1); + if (L.IsStringOrNumber(-2)) + { + var v = L.ToString(-2); + + request.Headers.Add(k, v); + } + else if (L.IsNil(-2)) + { + request.Headers.Remove(k); + } + else + { + L.ArgumentError(3, "string, number or nil expected, got " + L.TypeName(L.Type(-2)) + " in field " + k); + } + + L.Pop(2); + } + + L.Pop(1); + } + + var options = new Dictionary + { + ["binary"] = false, + }; + + if (L.IsTable(4)) // other options? + { + L.PushCopy(4); + L.PushNil(); + + while (L.Next(-2)) + { + L.PushCopy(-2); + var k = L.CheckString(-1); + + switch (k) + { + case "method": + options["method"] = L.CheckString(-2); + break; + case "binary": + options["binary"] = L.IsBoolean(-2) ? L.ToBoolean(-2) : false; + break; + } + + L.Pop(2); + } + + L.Pop(1); + } + + if (!L.IsNoneOrNil(2)) + { + if ((bool)options["binary"]) + { + request.Content = new ByteArrayContent(L.CheckBuffer(2)); + } + else + { + request.Content = new StringContent(L.CheckString(2)); + } + } + + request.Method = options.TryGetValue("method", out var value) + ? new HttpMethod((string)value) + : request.Content is not null ? HttpMethod.Post : HttpMethod.Get; + + var requestId = RequestId++; + + var reqTask = _client.SendAsync(request); + reqTask.ContinueWith(async (task) => + { + var response = await task; + object content; + if ((bool)options["binary"]) + content = await response.Content.ReadAsByteArrayAsync(); + else + content = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + _game.LuaRuntime.PushEvent("http_response", requestId, content, (int)response.StatusCode); + else + _game.LuaRuntime.PushEvent("http_failure", requestId, content, (int)response.StatusCode, response.ReasonPhrase); + }); + + L.PushInteger(requestId); + + return 1; + } + + private static int L_CheckUrl(IntPtr state) + { + var L = Lua.FromIntPtr(state); + + var url = L.CheckString(1); + + var isValid = TryGetUri(url, out _); + + L.PushBoolean(isValid); + + return 1; } } diff --git a/Capy64/LuaRuntime/Libraries/OS.cs b/Capy64/LuaRuntime/Libraries/OS.cs index 92e0e3b..1add392 100644 --- a/Capy64/LuaRuntime/Libraries/OS.cs +++ b/Capy64/LuaRuntime/Libraries/OS.cs @@ -34,7 +34,7 @@ public class OS : IPlugin { var L = Lua.FromIntPtr(state); - L.PushString(Capy64.Version); + L.PushString("Capy64 " + Capy64.Version); return 1; } diff --git a/Capy64/LuaRuntime/Runtime.cs b/Capy64/LuaRuntime/Runtime.cs index b60932c..e7f4bc8 100644 --- a/Capy64/LuaRuntime/Runtime.cs +++ b/Capy64/LuaRuntime/Runtime.cs @@ -124,7 +124,7 @@ public class Runtime { foreach (var par in ev.Parameters) { - Thread.PushValue(par); + Thread.PushObject(par); } } return (ev.Parameters?.Length ?? 0) + 1;