mirror of
https://github.com/lune-org/lune.git
synced 2025-04-09 21:10:53 +01:00
Compare commits
28 commits
Author | SHA1 | Date | |
---|---|---|---|
|
27e3efca97 | ||
|
8bd1a9b77d | ||
|
bb8c4bce82 | ||
|
6902ecaa7c | ||
|
dc08b91314 | ||
|
822dd19393 | ||
6cd0234a5f | |||
|
19e7f57284 | ||
|
5d1401cdf6 | ||
|
91af86cca2 | ||
|
c935149c1e | ||
|
e5bda57665 | ||
|
ef294f207c | ||
|
f89d02a60d | ||
|
d090cd2420 | ||
|
99c17795c1 | ||
|
138221b93e | ||
|
8abfc21181 | ||
309c461e11 | |||
|
93fa14d832 | ||
df4fb9be91 | |||
eaac9ff53a | |||
|
0d2f5539b6 | ||
|
0f4cac29aa | ||
|
010cd36375 | ||
|
c17da72815 | ||
|
ff83c401b8 | ||
|
a007fa94a6 |
86 changed files with 1698 additions and 695 deletions
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
|
@ -23,11 +23,8 @@ jobs:
|
||||||
with:
|
with:
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
|
|
||||||
- name: Install Just
|
|
||||||
uses: extractions/setup-just@v2
|
|
||||||
|
|
||||||
- name: Install Tooling
|
- name: Install Tooling
|
||||||
uses: ok-nick/setup-aftman@v0.4.2
|
uses: CompeyDev/setup-rokit@v0.1.2
|
||||||
|
|
||||||
- name: Check Formatting
|
- name: Check Formatting
|
||||||
run: just fmt-check
|
run: just fmt-check
|
||||||
|
@ -40,11 +37,8 @@ jobs:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Just
|
|
||||||
uses: extractions/setup-just@v2
|
|
||||||
|
|
||||||
- name: Install Tooling
|
- name: Install Tooling
|
||||||
uses: ok-nick/setup-aftman@v0.4.2
|
uses: CompeyDev/setup-rokit@v0.1.2
|
||||||
|
|
||||||
- name: Analyze
|
- name: Analyze
|
||||||
run: just analyze
|
run: just analyze
|
||||||
|
|
|
@ -129,7 +129,7 @@ end
|
||||||
]]
|
]]
|
||||||
|
|
||||||
print("Sending 4 pings to google 🌏")
|
print("Sending 4 pings to google 🌏")
|
||||||
local result = process.spawn("ping", {
|
local result = process.exec("ping", {
|
||||||
"google.com",
|
"google.com",
|
||||||
"-c 4",
|
"-c 4",
|
||||||
})
|
})
|
||||||
|
|
|
@ -28,8 +28,8 @@ end)
|
||||||
|
|
||||||
for _ = 1, 5 do
|
for _ = 1, 5 do
|
||||||
local start = os.clock()
|
local start = os.clock()
|
||||||
socket.send(tostring(1))
|
socket:send(tostring(1))
|
||||||
local response = socket.next()
|
local response = socket:next()
|
||||||
local elapsed = os.clock() - start
|
local elapsed = os.clock() - start
|
||||||
print(`Got response '{response}' in {elapsed * 1_000} milliseconds`)
|
print(`Got response '{response}' in {elapsed * 1_000} milliseconds`)
|
||||||
task.wait(1 - elapsed)
|
task.wait(1 - elapsed)
|
||||||
|
@ -38,7 +38,7 @@ end
|
||||||
-- Everything went well, and we are done with the socket, so we can close it
|
-- Everything went well, and we are done with the socket, so we can close it
|
||||||
|
|
||||||
print("Closing web socket...")
|
print("Closing web socket...")
|
||||||
socket.close()
|
socket:close()
|
||||||
|
|
||||||
task.cancel(forceExit)
|
task.cancel(forceExit)
|
||||||
print("Done! 🌙")
|
print("Done! 🌙")
|
||||||
|
|
|
@ -15,9 +15,9 @@ local handle = net.serve(PORT, {
|
||||||
handleWebSocket = function(socket)
|
handleWebSocket = function(socket)
|
||||||
print("Got new web socket connection!")
|
print("Got new web socket connection!")
|
||||||
repeat
|
repeat
|
||||||
local message = socket.next()
|
local message = socket:next()
|
||||||
if message ~= nil then
|
if message ~= nil then
|
||||||
socket.send("Echo - " .. message)
|
socket:send("Echo - " .. message)
|
||||||
end
|
end
|
||||||
until message == nil
|
until message == nil
|
||||||
print("Web socket disconnected.")
|
print("Web socket disconnected.")
|
||||||
|
|
54
CHANGELOG.md
54
CHANGELOG.md
|
@ -8,6 +8,60 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## `0.9.0`
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
- Added two new process spawning functions - `process.create` and `process.exec`, removing the previous `process.spawn` API completely. ([#211])
|
||||||
|
|
||||||
|
To migrate from `process.spawn`, use the new `process.exec` API which retains the same behavior as the old function.
|
||||||
|
|
||||||
|
The new `process.create` function is a non-blocking process creation API and can be used to interactively
|
||||||
|
read and write stdio of the process.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local child = process.create("program", {
|
||||||
|
"cli-argument",
|
||||||
|
"other-cli-argument"
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Writing to stdin
|
||||||
|
child.stdin:write("Hello from Lune!")
|
||||||
|
|
||||||
|
-- Reading from stdout
|
||||||
|
local data = child.stdout:read()
|
||||||
|
print(buffer.tostring(data))
|
||||||
|
```
|
||||||
|
|
||||||
|
- WebSocket methods in `net.socket` and `net.serve` now use standard Lua method calling convention and colon syntax.
|
||||||
|
This means `socket.send(...)` is now `socket:send(...)`, `socket.close(...)` is now `socket:close(...)`, and so on.
|
||||||
|
|
||||||
|
- `Runtime::run` now returns a more useful value instead of an `ExitCode` ([#178])
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Documentation comments for several standard library properties have been improved ([#248], [#250])
|
||||||
|
- Error messages no longer contain redundant or duplicate stack trace information
|
||||||
|
|
||||||
|
[#178]: https://github.com/lune-org/lune/pull/178
|
||||||
|
[#211]: https://github.com/lune-org/lune/pull/211
|
||||||
|
[#248]: https://github.com/lune-org/lune/pull/248
|
||||||
|
[#250]: https://github.com/lune-org/lune/pull/250
|
||||||
|
|
||||||
|
## `0.8.9` - October 7th, 2024
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated to Luau version `0.640`
|
||||||
|
|
||||||
|
## `0.8.8` - August 22nd, 2024
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed errors when deserializing `Lighting.AttributesSerialize` by updating `rbx-dom` dependencies ([#245])
|
||||||
|
|
||||||
|
[#245]: https://github.com/lune-org/lune/pull/245
|
||||||
|
|
||||||
## `0.8.7` - August 10th, 2024
|
## `0.8.7` - August 10th, 2024
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
720
Cargo.lock
generated
720
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,3 +0,0 @@
|
||||||
[tools]
|
|
||||||
luau-lsp = "JohnnyMorganz/luau-lsp@1.32.1"
|
|
||||||
stylua = "JohnnyMorganz/StyLua@0.20.0"
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "lune-roblox"
|
name = "lune-roblox"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
repository = "https://github.com/lune-org/lune"
|
repository = "https://github.com/lune-org/lune"
|
||||||
|
@ -20,10 +20,10 @@ rand = "0.8"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
once_cell = "1.17"
|
once_cell = "1.17"
|
||||||
|
|
||||||
rbx_binary = "0.7.3"
|
rbx_binary = "1.0.0"
|
||||||
rbx_dom_weak = "2.6.0"
|
rbx_dom_weak = "3.0.0"
|
||||||
rbx_reflection = "4.4.0"
|
rbx_reflection = "5.0.0"
|
||||||
rbx_reflection_database = "0.2.9"
|
rbx_reflection_database = "1.0.0"
|
||||||
rbx_xml = "0.13.2"
|
rbx_xml = "1.0.0"
|
||||||
|
|
||||||
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
||||||
|
|
|
@ -47,6 +47,7 @@ pub fn ensure_valid_attribute_value(value: &DomValue) -> LuaResult<()> {
|
||||||
| DomType::CFrame
|
| DomType::CFrame
|
||||||
| DomType::Color3
|
| DomType::Color3
|
||||||
| DomType::ColorSequence
|
| DomType::ColorSequence
|
||||||
|
| DomType::EnumItem
|
||||||
| DomType::Float32
|
| DomType::Float32
|
||||||
| DomType::Float64
|
| DomType::Float64
|
||||||
| DomType::Font
|
| DomType::Font
|
||||||
|
|
|
@ -51,7 +51,7 @@ impl<'lua> DomValueToLua<'lua> for LuaValue<'lua> {
|
||||||
DomValue::Float32(n) => Ok(LuaValue::Number(*n as f64)),
|
DomValue::Float32(n) => Ok(LuaValue::Number(*n as f64)),
|
||||||
DomValue::String(s) => Ok(LuaValue::String(lua.create_string(s)?)),
|
DomValue::String(s) => Ok(LuaValue::String(lua.create_string(s)?)),
|
||||||
DomValue::BinaryString(s) => Ok(LuaValue::String(lua.create_string(s)?)),
|
DomValue::BinaryString(s) => Ok(LuaValue::String(lua.create_string(s)?)),
|
||||||
DomValue::Content(s) => Ok(LuaValue::String(
|
DomValue::ContentId(s) => Ok(LuaValue::String(
|
||||||
lua.create_string(AsRef::<str>::as_ref(s))?,
|
lua.create_string(AsRef::<str>::as_ref(s))?,
|
||||||
)),
|
)),
|
||||||
|
|
||||||
|
@ -104,8 +104,8 @@ impl<'lua> LuaToDomValue<'lua> for LuaValue<'lua> {
|
||||||
(LuaValue::String(s), DomType::BinaryString) => {
|
(LuaValue::String(s), DomType::BinaryString) => {
|
||||||
Ok(DomValue::BinaryString(s.as_ref().into()))
|
Ok(DomValue::BinaryString(s.as_ref().into()))
|
||||||
}
|
}
|
||||||
(LuaValue::String(s), DomType::Content) => {
|
(LuaValue::String(s), DomType::ContentId) => {
|
||||||
Ok(DomValue::Content(s.to_str()?.to_string().into()))
|
Ok(DomValue::ContentId(s.to_str()?.to_string().into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Some values are either optional or default and we
|
// NOTE: Some values are either optional or default and we
|
||||||
|
@ -200,6 +200,8 @@ impl<'lua> DomValueToLua<'lua> for LuaAnyUserData<'lua> {
|
||||||
DomValue::Color3(value) => dom_to_userdata!(lua, value => Color3),
|
DomValue::Color3(value) => dom_to_userdata!(lua, value => Color3),
|
||||||
DomValue::Color3uint8(value) => dom_to_userdata!(lua, value => Color3),
|
DomValue::Color3uint8(value) => dom_to_userdata!(lua, value => Color3),
|
||||||
DomValue::ColorSequence(value) => dom_to_userdata!(lua, value => ColorSequence),
|
DomValue::ColorSequence(value) => dom_to_userdata!(lua, value => ColorSequence),
|
||||||
|
DomValue::Content(value) => dom_to_userdata!(lua, value => Content),
|
||||||
|
DomValue::EnumItem(value) => dom_to_userdata!(lua, value => EnumItem),
|
||||||
DomValue::Faces(value) => dom_to_userdata!(lua, value => Faces),
|
DomValue::Faces(value) => dom_to_userdata!(lua, value => Faces),
|
||||||
DomValue::Font(value) => dom_to_userdata!(lua, value => Font),
|
DomValue::Font(value) => dom_to_userdata!(lua, value => Font),
|
||||||
DomValue::NumberRange(value) => dom_to_userdata!(lua, value => NumberRange),
|
DomValue::NumberRange(value) => dom_to_userdata!(lua, value => NumberRange),
|
||||||
|
@ -256,7 +258,8 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> {
|
||||||
DomType::Color3 => userdata_to_dom!(self as Color3 => dom::Color3),
|
DomType::Color3 => userdata_to_dom!(self as Color3 => dom::Color3),
|
||||||
DomType::Color3uint8 => userdata_to_dom!(self as Color3 => dom::Color3uint8),
|
DomType::Color3uint8 => userdata_to_dom!(self as Color3 => dom::Color3uint8),
|
||||||
DomType::ColorSequence => userdata_to_dom!(self as ColorSequence => dom::ColorSequence),
|
DomType::ColorSequence => userdata_to_dom!(self as ColorSequence => dom::ColorSequence),
|
||||||
DomType::Enum => userdata_to_dom!(self as EnumItem => dom::Enum),
|
DomType::Content => userdata_to_dom!(self as Content => dom::Content),
|
||||||
|
DomType::EnumItem => userdata_to_dom!(self as EnumItem => dom::EnumItem),
|
||||||
DomType::Faces => userdata_to_dom!(self as Faces => dom::Faces),
|
DomType::Faces => userdata_to_dom!(self as Faces => dom::Faces),
|
||||||
DomType::Font => userdata_to_dom!(self as Font => dom::Font),
|
DomType::Font => userdata_to_dom!(self as Font => dom::Font),
|
||||||
DomType::NumberRange => userdata_to_dom!(self as NumberRange => dom::NumberRange),
|
DomType::NumberRange => userdata_to_dom!(self as NumberRange => dom::NumberRange),
|
||||||
|
@ -314,7 +317,7 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> {
|
||||||
value if value.is::<CFrame>() => userdata_to_dom!(value as CFrame => dom::CFrame),
|
value if value.is::<CFrame>() => userdata_to_dom!(value as CFrame => dom::CFrame),
|
||||||
value if value.is::<Color3>() => userdata_to_dom!(value as Color3 => dom::Color3),
|
value if value.is::<Color3>() => userdata_to_dom!(value as Color3 => dom::Color3),
|
||||||
value if value.is::<ColorSequence>() => userdata_to_dom!(value as ColorSequence => dom::ColorSequence),
|
value if value.is::<ColorSequence>() => userdata_to_dom!(value as ColorSequence => dom::ColorSequence),
|
||||||
value if value.is::<Enum>() => userdata_to_dom!(value as EnumItem => dom::Enum),
|
value if value.is::<EnumItem>() => userdata_to_dom!(value as EnumItem => dom::EnumItem),
|
||||||
value if value.is::<Faces>() => userdata_to_dom!(value as Faces => dom::Faces),
|
value if value.is::<Faces>() => userdata_to_dom!(value as Faces => dom::Faces),
|
||||||
value if value.is::<Font>() => userdata_to_dom!(value as Font => dom::Font),
|
value if value.is::<Font>() => userdata_to_dom!(value as Font => dom::Font),
|
||||||
value if value.is::<Instance>() => userdata_to_dom!(value as Instance => dom::Ref),
|
value if value.is::<Instance>() => userdata_to_dom!(value as Instance => dom::Ref),
|
||||||
|
|
|
@ -19,7 +19,9 @@ impl DomValueExt for DomType {
|
||||||
Color3uint8 => "Color3uint8",
|
Color3uint8 => "Color3uint8",
|
||||||
ColorSequence => "ColorSequence",
|
ColorSequence => "ColorSequence",
|
||||||
Content => "Content",
|
Content => "Content",
|
||||||
|
ContentId => "ContentId",
|
||||||
Enum => "Enum",
|
Enum => "Enum",
|
||||||
|
EnumItem => "EnumItem",
|
||||||
Faces => "Faces",
|
Faces => "Faces",
|
||||||
Float32 => "Float32",
|
Float32 => "Float32",
|
||||||
Float64 => "Float64",
|
Float64 => "Float64",
|
||||||
|
|
120
crates/lune-roblox/src/datatypes/types/content.rs
Normal file
120
crates/lune-roblox/src/datatypes/types/content.rs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
use mlua::prelude::*;
|
||||||
|
use rbx_dom_weak::types::{Content as DomContent, ContentType};
|
||||||
|
|
||||||
|
use lune_utils::TableBuilder;
|
||||||
|
|
||||||
|
use crate::{exports::LuaExportsTable, instance::Instance};
|
||||||
|
|
||||||
|
use super::{super::*, EnumItem};
|
||||||
|
|
||||||
|
/**
|
||||||
|
An implementation of the [Content](https://create.roblox.com/docs/reference/engine/datatypes/Content) Roblox datatype.
|
||||||
|
|
||||||
|
This implements all documented properties, methods & constructors of the Content type as of April 2025.
|
||||||
|
*/
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Content(ContentType);
|
||||||
|
|
||||||
|
impl LuaExportsTable<'_> for Content {
|
||||||
|
const EXPORT_NAME: &'static str = "Content";
|
||||||
|
|
||||||
|
fn create_exports_table(lua: &'_ Lua) -> LuaResult<LuaTable<'_>> {
|
||||||
|
let from_uri = |_, uri: String| Ok(Self(ContentType::Uri(uri)));
|
||||||
|
|
||||||
|
let from_object = |_, obj: LuaUserDataRef<Instance>| {
|
||||||
|
let database = rbx_reflection_database::get();
|
||||||
|
let instance_descriptor = database
|
||||||
|
.classes
|
||||||
|
.get("Instance")
|
||||||
|
.expect("the reflection database should always have Instance in it");
|
||||||
|
let param_descriptor = database.classes.get(obj.get_class_name()).expect(
|
||||||
|
"you should not be able to construct an Instance that is not known to Lune",
|
||||||
|
);
|
||||||
|
if database.has_superclass(param_descriptor, instance_descriptor) {
|
||||||
|
Err(LuaError::runtime("the provided object is a descendant class of 'Instance', expected one that was only an 'Object'"))
|
||||||
|
} else {
|
||||||
|
Ok(Content(ContentType::Object(obj.dom_ref)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TableBuilder::new(lua)?
|
||||||
|
.with_value("none", Content(ContentType::None))?
|
||||||
|
.with_function("fromUri", from_uri)?
|
||||||
|
.with_function("fromObject", from_object)?
|
||||||
|
.build_readonly()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LuaUserData for Content {
|
||||||
|
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||||
|
fields.add_field_method_get("SourceType", |_, this| {
|
||||||
|
let variant_name = match &this.0 {
|
||||||
|
ContentType::None => "None",
|
||||||
|
ContentType::Uri(_) => "Uri",
|
||||||
|
ContentType::Object(_) => "Object",
|
||||||
|
other => {
|
||||||
|
return Err(LuaError::runtime(format!(
|
||||||
|
"cannot get SourceType: unknown ContentType variant '{other:?}'"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(EnumItem::from_enum_name_and_name(
|
||||||
|
"ContentSourceType",
|
||||||
|
variant_name,
|
||||||
|
))
|
||||||
|
});
|
||||||
|
fields.add_field_method_get("Uri", |_, this| {
|
||||||
|
if let ContentType::Uri(uri) = &this.0 {
|
||||||
|
Ok(Some(uri.to_owned()))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fields.add_field_method_get("Object", |_, this| {
|
||||||
|
if let ContentType::Object(referent) = &this.0 {
|
||||||
|
Ok(Instance::new_opt(*referent))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||||
|
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||||
|
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Content {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
// Regardless of the actual content of the Content, Roblox just emits
|
||||||
|
// `Content` when casting it to a string. We do not do that.
|
||||||
|
write!(f, "Content(")?;
|
||||||
|
match &self.0 {
|
||||||
|
ContentType::None => write!(f, "None")?,
|
||||||
|
ContentType::Uri(uri) => write!(f, "Uri={uri}")?,
|
||||||
|
ContentType::Object(_) => write!(f, "Object")?,
|
||||||
|
other => write!(f, "UnknownType({other:?})")?,
|
||||||
|
}
|
||||||
|
write!(f, ")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DomContent> for Content {
|
||||||
|
fn from(value: DomContent) -> Self {
|
||||||
|
Self(value.value().clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Content> for DomContent {
|
||||||
|
fn from(value: Content) -> Self {
|
||||||
|
match value.0 {
|
||||||
|
ContentType::None => Self::none(),
|
||||||
|
ContentType::Uri(uri) => Self::from_uri(uri),
|
||||||
|
ContentType::Object(referent) => Self::from_referent(referent),
|
||||||
|
other => unimplemented!("unknown variant of ContentType: {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
|
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
use rbx_dom_weak::types::Enum as DomEnum;
|
use rbx_dom_weak::types::EnumItem as DomEnumItem;
|
||||||
|
|
||||||
use super::{super::*, Enum};
|
use super::{super::*, Enum};
|
||||||
|
|
||||||
|
@ -100,8 +100,18 @@ impl PartialEq for EnumItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<EnumItem> for DomEnum {
|
impl From<EnumItem> for DomEnumItem {
|
||||||
fn from(v: EnumItem) -> Self {
|
fn from(v: EnumItem) -> Self {
|
||||||
DomEnum::from_u32(v.value)
|
DomEnumItem {
|
||||||
|
ty: v.parent.desc.name.to_string(),
|
||||||
|
value: v.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DomEnumItem> for EnumItem {
|
||||||
|
fn from(value: DomEnumItem) -> Self {
|
||||||
|
EnumItem::from_enum_name_and_value(value.ty, value.value)
|
||||||
|
.expect("cannot convert rbx_type::EnumItem with unknown type into EnumItem")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ mod cframe;
|
||||||
mod color3;
|
mod color3;
|
||||||
mod color_sequence;
|
mod color_sequence;
|
||||||
mod color_sequence_keypoint;
|
mod color_sequence_keypoint;
|
||||||
|
mod content;
|
||||||
mod r#enum;
|
mod r#enum;
|
||||||
mod r#enum_item;
|
mod r#enum_item;
|
||||||
mod r#enums;
|
mod r#enums;
|
||||||
|
@ -30,6 +31,7 @@ pub use cframe::CFrame;
|
||||||
pub use color3::Color3;
|
pub use color3::Color3;
|
||||||
pub use color_sequence::ColorSequence;
|
pub use color_sequence::ColorSequence;
|
||||||
pub use color_sequence_keypoint::ColorSequenceKeypoint;
|
pub use color_sequence_keypoint::ColorSequenceKeypoint;
|
||||||
|
pub use content::Content;
|
||||||
pub use faces::Faces;
|
pub use faces::Faces;
|
||||||
pub use font::Font;
|
pub use font::Font;
|
||||||
pub use number_range::NumberRange;
|
pub use number_range::NumberRange;
|
||||||
|
|
|
@ -65,7 +65,7 @@ impl DocumentKind {
|
||||||
for child_ref in dom.root().children() {
|
for child_ref in dom.root().children() {
|
||||||
if let Some(child_inst) = dom.get_by_ref(*child_ref) {
|
if let Some(child_inst) = dom.get_by_ref(*child_ref) {
|
||||||
has_top_level_child = true;
|
has_top_level_child = true;
|
||||||
if class_is_a_service(&child_inst.class).unwrap_or(false) {
|
if class_is_a_service(child_inst.class).unwrap_or(false) {
|
||||||
has_top_level_service = true;
|
has_top_level_service = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use rbx_dom_weak::{
|
use rbx_dom_weak::{
|
||||||
types::{Ref as DomRef, VariantType as DomType},
|
types::{Ref as DomRef, VariantType as DomType},
|
||||||
Instance as DomInstance, WeakDom,
|
ustr, Instance as DomInstance, WeakDom,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::shared::instance::class_is_a;
|
use crate::shared::instance::class_is_a;
|
||||||
|
@ -18,8 +18,8 @@ pub fn postprocess_dom_for_model(dom: &mut WeakDom) {
|
||||||
remove_matching_prop(inst, DomType::UniqueId, "HistoryId");
|
remove_matching_prop(inst, DomType::UniqueId, "HistoryId");
|
||||||
// Similar story with ScriptGuid - this is used
|
// Similar story with ScriptGuid - this is used
|
||||||
// in the studio-only cloud script drafts feature
|
// in the studio-only cloud script drafts feature
|
||||||
if class_is_a(&inst.class, "LuaSourceContainer").unwrap_or(false) {
|
if class_is_a(inst.class, "LuaSourceContainer").unwrap_or(false) {
|
||||||
inst.properties.remove("ScriptGuid");
|
inst.properties.remove(&ustr("ScriptGuid"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,8 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_matching_prop(inst: &mut DomInstance, ty: DomType, name: &'static str) {
|
fn remove_matching_prop(inst: &mut DomInstance, ty: DomType, name: &'static str) {
|
||||||
if inst.properties.get(name).map_or(false, |u| u.ty() == ty) {
|
let name = &ustr(name);
|
||||||
|
if inst.properties.get(name).is_some_and(|u| u.ty() == ty) {
|
||||||
inst.properties.remove(name);
|
inst.properties.remove(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
||||||
"FindFirstAncestorWhichIsA",
|
"FindFirstAncestorWhichIsA",
|
||||||
|lua, this, class_name: String| {
|
|lua, this, class_name: String| {
|
||||||
ensure_not_destroyed(this)?;
|
ensure_not_destroyed(this)?;
|
||||||
this.find_ancestor(|child| class_is_a(&child.class, &class_name).unwrap_or(false))
|
this.find_ancestor(|child| class_is_a(child.class, &class_name).unwrap_or(false))
|
||||||
.into_lua(lua)
|
.into_lua(lua)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -104,7 +104,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
||||||
|lua, this, (class_name, recursive): (String, Option<bool>)| {
|
|lua, this, (class_name, recursive): (String, Option<bool>)| {
|
||||||
ensure_not_destroyed(this)?;
|
ensure_not_destroyed(this)?;
|
||||||
let predicate =
|
let predicate =
|
||||||
|child: &DomInstance| class_is_a(&child.class, &class_name).unwrap_or(false);
|
|child: &DomInstance| class_is_a(child.class, &class_name).unwrap_or(false);
|
||||||
if matches!(recursive, Some(true)) {
|
if matches!(recursive, Some(true)) {
|
||||||
this.find_descendant(predicate).into_lua(lua)
|
this.find_descendant(predicate).into_lua(lua)
|
||||||
} else {
|
} else {
|
||||||
|
@ -113,8 +113,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
m.add_method("IsA", |_, this, class_name: String| {
|
m.add_method("IsA", |_, this, class_name: String| {
|
||||||
ensure_not_destroyed(this)?;
|
Ok(class_is_a(this.class_name, class_name).unwrap_or(false))
|
||||||
Ok(class_is_a(&this.class_name, class_name).unwrap_or(false))
|
|
||||||
});
|
});
|
||||||
m.add_method(
|
m.add_method(
|
||||||
"IsAncestorOf",
|
"IsAncestorOf",
|
||||||
|
@ -217,20 +216,21 @@ fn instance_property_get<'lua>(
|
||||||
this: &Instance,
|
this: &Instance,
|
||||||
prop_name: String,
|
prop_name: String,
|
||||||
) -> LuaResult<LuaValue<'lua>> {
|
) -> LuaResult<LuaValue<'lua>> {
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
|
|
||||||
match prop_name.as_str() {
|
match prop_name.as_str() {
|
||||||
"ClassName" => return this.get_class_name().into_lua(lua),
|
"ClassName" => return this.get_class_name().into_lua(lua),
|
||||||
"Name" => {
|
|
||||||
return this.get_name().into_lua(lua);
|
|
||||||
}
|
|
||||||
"Parent" => {
|
"Parent" => {
|
||||||
return this.get_parent().into_lua(lua);
|
return this.get_parent().into_lua(lua);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(info) = find_property_info(&this.class_name, &prop_name) {
|
ensure_not_destroyed(this)?;
|
||||||
|
|
||||||
|
if prop_name.as_str() == "Name" {
|
||||||
|
return this.get_name().into_lua(lua);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(info) = find_property_info(this.class_name, &prop_name) {
|
||||||
if let Some(prop) = this.get_property(&prop_name) {
|
if let Some(prop) = this.get_property(&prop_name) {
|
||||||
if let DomValue::Enum(enum_value) = prop {
|
if let DomValue::Enum(enum_value) = prop {
|
||||||
let enum_name = info.enum_name.ok_or_else(|| {
|
let enum_name = info.enum_name.ok_or_else(|| {
|
||||||
|
@ -275,7 +275,7 @@ fn instance_property_get<'lua>(
|
||||||
} else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) {
|
} else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) {
|
||||||
Ok(LuaValue::UserData(lua.create_userdata(inst)?))
|
Ok(LuaValue::UserData(lua.create_userdata(inst)?))
|
||||||
} else if let Some(getter) = InstanceRegistry::find_property_getter(lua, this, &prop_name) {
|
} else if let Some(getter) = InstanceRegistry::find_property_getter(lua, this, &prop_name) {
|
||||||
getter.call(this.clone())
|
getter.call(*this)
|
||||||
} else if let Some(method) = InstanceRegistry::find_method(lua, this, &prop_name) {
|
} else if let Some(method) = InstanceRegistry::find_method(lua, this, &prop_name) {
|
||||||
Ok(LuaValue::Function(method))
|
Ok(LuaValue::Function(method))
|
||||||
} else {
|
} else {
|
||||||
|
@ -321,17 +321,17 @@ fn instance_property_set<'lua>(
|
||||||
}
|
}
|
||||||
type Parent<'lua> = Option<LuaUserDataRef<'lua, Instance>>;
|
type Parent<'lua> = Option<LuaUserDataRef<'lua, Instance>>;
|
||||||
let parent = Parent::from_lua(prop_value, lua)?;
|
let parent = Parent::from_lua(prop_value, lua)?;
|
||||||
this.set_parent(parent.map(|p| p.clone()));
|
this.set_parent(parent.map(|p| *p));
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(info) = find_property_info(&this.class_name, &prop_name) {
|
if let Some(info) = find_property_info(this.class_name, &prop_name) {
|
||||||
if let Some(enum_name) = info.enum_name {
|
if let Some(enum_name) = info.enum_name {
|
||||||
match LuaUserDataRef::<EnumItem>::from_lua(prop_value, lua) {
|
match LuaUserDataRef::<EnumItem>::from_lua(prop_value, lua) {
|
||||||
Ok(given_enum) if given_enum.parent.desc.name == enum_name => {
|
Ok(given_enum) if given_enum.parent.desc.name == enum_name => {
|
||||||
this.set_property(prop_name, DomValue::Enum((*given_enum).clone().into()));
|
this.set_property(prop_name, DomValue::EnumItem((*given_enum).clone().into()));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Ok(given_enum) => Err(LuaError::RuntimeError(format!(
|
Ok(given_enum) => Err(LuaError::RuntimeError(format!(
|
||||||
|
@ -354,7 +354,7 @@ fn instance_property_set<'lua>(
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
} else if let Some(setter) = InstanceRegistry::find_property_setter(lua, this, &prop_name) {
|
} else if let Some(setter) = InstanceRegistry::find_property_setter(lua, this, &prop_name) {
|
||||||
setter.call((this.clone(), prop_value))
|
setter.call((*this, prop_value))
|
||||||
} else {
|
} else {
|
||||||
Err(LuaError::RuntimeError(format!(
|
Err(LuaError::RuntimeError(format!(
|
||||||
"{prop_name} is not a valid member of {this}",
|
"{prop_name} is not a valid member of {this}",
|
||||||
|
|
|
@ -48,7 +48,7 @@ fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> Lua
|
||||||
Ok(service)
|
Ok(service)
|
||||||
} else {
|
} else {
|
||||||
let service = Instance::new_orphaned(service_name);
|
let service = Instance::new_orphaned(service_name);
|
||||||
service.set_parent(Some(this.clone()));
|
service.set_parent(Some(*this));
|
||||||
Ok(service)
|
Ok(service)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ use mlua::prelude::*;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use rbx_dom_weak::{
|
use rbx_dom_weak::{
|
||||||
types::{Attributes as DomAttributes, Ref as DomRef, Variant as DomValue},
|
types::{Attributes as DomAttributes, Ref as DomRef, Variant as DomValue},
|
||||||
Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, WeakDom,
|
ustr, Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, Ustr, WeakDom,
|
||||||
};
|
};
|
||||||
|
|
||||||
use lune_utils::TableBuilder;
|
use lune_utils::TableBuilder;
|
||||||
|
@ -34,10 +34,10 @@ const PROPERTY_NAME_TAGS: &str = "Tags";
|
||||||
static INTERNAL_DOM: Lazy<Mutex<WeakDom>> =
|
static INTERNAL_DOM: Lazy<Mutex<WeakDom>> =
|
||||||
Lazy::new(|| Mutex::new(WeakDom::new(DomInstanceBuilder::new("ROOT"))));
|
Lazy::new(|| Mutex::new(WeakDom::new(DomInstanceBuilder::new("ROOT"))));
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct Instance {
|
pub struct Instance {
|
||||||
pub(crate) dom_ref: DomRef,
|
pub(crate) dom_ref: DomRef,
|
||||||
pub(crate) class_name: String,
|
pub(crate) class_name: Ustr,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Instance {
|
impl Instance {
|
||||||
|
@ -75,7 +75,7 @@ impl Instance {
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
dom_ref,
|
dom_ref,
|
||||||
class_name: instance.class.clone(),
|
class_name: instance.class,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -96,14 +96,14 @@ impl Instance {
|
||||||
|
|
||||||
let class_name = class_name.as_ref();
|
let class_name = class_name.as_ref();
|
||||||
|
|
||||||
let instance = DomInstanceBuilder::new(class_name.to_string());
|
let instance = DomInstanceBuilder::new(class_name);
|
||||||
|
|
||||||
let dom_root = dom.root_ref();
|
let dom_root = dom.root_ref();
|
||||||
let dom_ref = dom.insert(dom_root, instance);
|
let dom_ref = dom.insert(dom_root, instance);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
dom_ref,
|
dom_ref,
|
||||||
class_name: class_name.to_string(),
|
class_name: ustr(class_name),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,7 +244,7 @@ impl Instance {
|
||||||
on the Roblox Developer Hub
|
on the Roblox Developer Hub
|
||||||
*/
|
*/
|
||||||
pub fn is_a(&self, class_name: impl AsRef<str>) -> bool {
|
pub fn is_a(&self, class_name: impl AsRef<str>) -> bool {
|
||||||
class_is_a(&self.class_name, class_name).unwrap_or(false)
|
class_is_a(self.class_name, class_name).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -302,10 +302,7 @@ impl Instance {
|
||||||
pub fn get_parent(&self) -> Option<Instance> {
|
pub fn get_parent(&self) -> Option<Instance> {
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||||
|
|
||||||
let parent_ref = dom
|
let parent_ref = dom.get_by_ref(self.dom_ref)?.parent();
|
||||||
.get_by_ref(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document")
|
|
||||||
.parent();
|
|
||||||
|
|
||||||
if parent_ref == dom.root_ref() {
|
if parent_ref == dom.root_ref() {
|
||||||
None
|
None
|
||||||
|
@ -344,7 +341,7 @@ impl Instance {
|
||||||
.get_by_ref(self.dom_ref)
|
.get_by_ref(self.dom_ref)
|
||||||
.expect("Failed to find instance in document")
|
.expect("Failed to find instance in document")
|
||||||
.properties
|
.properties
|
||||||
.get(name.as_ref())
|
.get(&ustr(name.as_ref()))
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -361,7 +358,7 @@ impl Instance {
|
||||||
.get_by_ref_mut(self.dom_ref)
|
.get_by_ref_mut(self.dom_ref)
|
||||||
.expect("Failed to find instance in document")
|
.expect("Failed to find instance in document")
|
||||||
.properties
|
.properties
|
||||||
.insert(name.as_ref().to_string(), value);
|
.insert(ustr(name.as_ref()), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -377,7 +374,7 @@ impl Instance {
|
||||||
.get_by_ref(self.dom_ref)
|
.get_by_ref(self.dom_ref)
|
||||||
.expect("Failed to find instance in document");
|
.expect("Failed to find instance in document");
|
||||||
if let Some(DomValue::Attributes(attributes)) =
|
if let Some(DomValue::Attributes(attributes)) =
|
||||||
inst.properties.get(PROPERTY_NAME_ATTRIBUTES)
|
inst.properties.get(&ustr(PROPERTY_NAME_ATTRIBUTES))
|
||||||
{
|
{
|
||||||
attributes.get(name.as_ref()).cloned()
|
attributes.get(name.as_ref()).cloned()
|
||||||
} else {
|
} else {
|
||||||
|
@ -398,7 +395,7 @@ impl Instance {
|
||||||
.get_by_ref(self.dom_ref)
|
.get_by_ref(self.dom_ref)
|
||||||
.expect("Failed to find instance in document");
|
.expect("Failed to find instance in document");
|
||||||
if let Some(DomValue::Attributes(attributes)) =
|
if let Some(DomValue::Attributes(attributes)) =
|
||||||
inst.properties.get(PROPERTY_NAME_ATTRIBUTES)
|
inst.properties.get(&ustr(PROPERTY_NAME_ATTRIBUTES))
|
||||||
{
|
{
|
||||||
attributes.clone().into_iter().collect()
|
attributes.clone().into_iter().collect()
|
||||||
} else {
|
} else {
|
||||||
|
@ -425,14 +422,14 @@ impl Instance {
|
||||||
value => value,
|
value => value,
|
||||||
};
|
};
|
||||||
if let Some(DomValue::Attributes(attributes)) =
|
if let Some(DomValue::Attributes(attributes)) =
|
||||||
inst.properties.get_mut(PROPERTY_NAME_ATTRIBUTES)
|
inst.properties.get_mut(&ustr(PROPERTY_NAME_ATTRIBUTES))
|
||||||
{
|
{
|
||||||
attributes.insert(name.as_ref().to_string(), value);
|
attributes.insert(name.as_ref().to_string(), value);
|
||||||
} else {
|
} else {
|
||||||
let mut attributes = DomAttributes::new();
|
let mut attributes = DomAttributes::new();
|
||||||
attributes.insert(name.as_ref().to_string(), value);
|
attributes.insert(name.as_ref().to_string(), value);
|
||||||
inst.properties.insert(
|
inst.properties.insert(
|
||||||
PROPERTY_NAME_ATTRIBUTES.to_string(),
|
ustr(PROPERTY_NAME_ATTRIBUTES),
|
||||||
DomValue::Attributes(attributes),
|
DomValue::Attributes(attributes),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -452,11 +449,11 @@ impl Instance {
|
||||||
.get_by_ref_mut(self.dom_ref)
|
.get_by_ref_mut(self.dom_ref)
|
||||||
.expect("Failed to find instance in document");
|
.expect("Failed to find instance in document");
|
||||||
if let Some(DomValue::Attributes(attributes)) =
|
if let Some(DomValue::Attributes(attributes)) =
|
||||||
inst.properties.get_mut(PROPERTY_NAME_ATTRIBUTES)
|
inst.properties.get_mut(&ustr(PROPERTY_NAME_ATTRIBUTES))
|
||||||
{
|
{
|
||||||
attributes.remove(name.as_ref());
|
attributes.remove(name.as_ref());
|
||||||
if attributes.is_empty() {
|
if attributes.is_empty() {
|
||||||
inst.properties.remove(PROPERTY_NAME_ATTRIBUTES);
|
inst.properties.remove(&ustr(PROPERTY_NAME_ATTRIBUTES));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -473,11 +470,11 @@ impl Instance {
|
||||||
let inst = dom
|
let inst = dom
|
||||||
.get_by_ref_mut(self.dom_ref)
|
.get_by_ref_mut(self.dom_ref)
|
||||||
.expect("Failed to find instance in document");
|
.expect("Failed to find instance in document");
|
||||||
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(PROPERTY_NAME_TAGS) {
|
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(&ustr(PROPERTY_NAME_TAGS)) {
|
||||||
tags.push(name.as_ref());
|
tags.push(name.as_ref());
|
||||||
} else {
|
} else {
|
||||||
inst.properties.insert(
|
inst.properties.insert(
|
||||||
PROPERTY_NAME_TAGS.to_string(),
|
ustr(PROPERTY_NAME_TAGS),
|
||||||
DomValue::Tags(vec![name.as_ref().to_string()].into()),
|
DomValue::Tags(vec![name.as_ref().to_string()].into()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -495,7 +492,7 @@ impl Instance {
|
||||||
let inst = dom
|
let inst = dom
|
||||||
.get_by_ref(self.dom_ref)
|
.get_by_ref(self.dom_ref)
|
||||||
.expect("Failed to find instance in document");
|
.expect("Failed to find instance in document");
|
||||||
if let Some(DomValue::Tags(tags)) = inst.properties.get(PROPERTY_NAME_TAGS) {
|
if let Some(DomValue::Tags(tags)) = inst.properties.get(&ustr(PROPERTY_NAME_TAGS)) {
|
||||||
tags.iter().map(ToString::to_string).collect()
|
tags.iter().map(ToString::to_string).collect()
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
|
@ -514,7 +511,7 @@ impl Instance {
|
||||||
let inst = dom
|
let inst = dom
|
||||||
.get_by_ref(self.dom_ref)
|
.get_by_ref(self.dom_ref)
|
||||||
.expect("Failed to find instance in document");
|
.expect("Failed to find instance in document");
|
||||||
if let Some(DomValue::Tags(tags)) = inst.properties.get(PROPERTY_NAME_TAGS) {
|
if let Some(DomValue::Tags(tags)) = inst.properties.get(&ustr(PROPERTY_NAME_TAGS)) {
|
||||||
let name = name.as_ref();
|
let name = name.as_ref();
|
||||||
tags.iter().any(|tag| tag == name)
|
tags.iter().any(|tag| tag == name)
|
||||||
} else {
|
} else {
|
||||||
|
@ -534,14 +531,12 @@ impl Instance {
|
||||||
let inst = dom
|
let inst = dom
|
||||||
.get_by_ref_mut(self.dom_ref)
|
.get_by_ref_mut(self.dom_ref)
|
||||||
.expect("Failed to find instance in document");
|
.expect("Failed to find instance in document");
|
||||||
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(PROPERTY_NAME_TAGS) {
|
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(&ustr(PROPERTY_NAME_TAGS)) {
|
||||||
let name = name.as_ref();
|
let name = name.as_ref();
|
||||||
let mut new_tags = tags.iter().map(ToString::to_string).collect::<Vec<_>>();
|
let mut new_tags = tags.iter().map(ToString::to_string).collect::<Vec<_>>();
|
||||||
new_tags.retain(|tag| tag != name);
|
new_tags.retain(|tag| tag != name);
|
||||||
inst.properties.insert(
|
inst.properties
|
||||||
PROPERTY_NAME_TAGS.to_string(),
|
.insert(ustr(PROPERTY_NAME_TAGS), DomValue::Tags(new_tags.into()));
|
||||||
DomValue::Tags(new_tags.into()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,9 +27,8 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(methods: &mut M)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_or_create_material_colors(instance: &Instance) -> MaterialColors {
|
fn get_or_create_material_colors(instance: &Instance) -> MaterialColors {
|
||||||
if let Some(Variant::MaterialColors(material_colors)) = instance.get_property("MaterialColors")
|
if let Some(Variant::MaterialColors(inner)) = instance.get_property("MaterialColors") {
|
||||||
{
|
inner
|
||||||
material_colors
|
|
||||||
} else {
|
} else {
|
||||||
MaterialColors::default()
|
MaterialColors::default()
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ fn create_all_exports(lua: &Lua) -> LuaResult<Vec<(&'static str, LuaValue)>> {
|
||||||
export::<Color3>(lua)?,
|
export::<Color3>(lua)?,
|
||||||
export::<ColorSequence>(lua)?,
|
export::<ColorSequence>(lua)?,
|
||||||
export::<ColorSequenceKeypoint>(lua)?,
|
export::<ColorSequenceKeypoint>(lua)?,
|
||||||
|
export::<Content>(lua)?,
|
||||||
export::<Faces>(lua)?,
|
export::<Faces>(lua)?,
|
||||||
export::<Font>(lua)?,
|
export::<Font>(lua)?,
|
||||||
export::<NumberRange>(lua)?,
|
export::<NumberRange>(lua)?,
|
||||||
|
|
|
@ -122,7 +122,7 @@ pub(crate) fn get_or_create_property_ref_instance(
|
||||||
Ok(inst)
|
Ok(inst)
|
||||||
} else {
|
} else {
|
||||||
let inst = Instance::new_orphaned(class_name);
|
let inst = Instance::new_orphaned(class_name);
|
||||||
inst.set_parent(Some(this.clone()));
|
inst.set_parent(Some(*this));
|
||||||
this.set_property(prop_name, DomValue::Ref(inst.dom_ref));
|
this.set_property(prop_name, DomValue::Ref(inst.dom_ref));
|
||||||
Ok(inst)
|
Ok(inst)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ pub fn make_list_writer() -> Box<ListWriter> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
Userdata metamethod implementations
|
Userdata metamethod implementations
|
||||||
|
|
||||||
Note that many of these return [`LuaResult`] even though they don't
|
Note that many of these return [`LuaResult`] even though they don't
|
||||||
|
|
|
@ -60,7 +60,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
Conversion methods between `DateTimeValues` and plain lua tables
|
Conversion methods between `DateTimeValues` and plain lua tables
|
||||||
|
|
||||||
Note that the `IntoLua` implementation here uses a read-only table,
|
Note that the `IntoLua` implementation here uses a read-only table,
|
||||||
|
@ -117,7 +117,7 @@ impl IntoLua<'_> for DateTimeValues {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
Conversion methods between chrono's timezone-aware `DateTime` to
|
Conversion methods between chrono's timezone-aware `DateTime` to
|
||||||
and from our non-timezone-aware `DateTimeValues` values struct
|
and from our non-timezone-aware `DateTimeValues` values struct
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
|
|
||||||
use lune_utils::TableBuilder;
|
use lune_utils::{jit::JitStatus, TableBuilder};
|
||||||
|
|
||||||
mod options;
|
mod options;
|
||||||
|
|
||||||
|
@ -78,7 +78,13 @@ fn load_source<'lua>(
|
||||||
// changed, otherwise disable JIT since it'll fall back anyways
|
// changed, otherwise disable JIT since it'll fall back anyways
|
||||||
lua.enable_jit(options.codegen_enabled && !env_changed);
|
lua.enable_jit(options.codegen_enabled && !env_changed);
|
||||||
let function = chunk.into_function()?;
|
let function = chunk.into_function()?;
|
||||||
lua.enable_jit(true);
|
lua.enable_jit(
|
||||||
|
lua.app_data_ref::<JitStatus>()
|
||||||
|
.ok_or(LuaError::runtime(
|
||||||
|
"Failed to get current JitStatus ref from AppData",
|
||||||
|
))?
|
||||||
|
.enabled(),
|
||||||
|
);
|
||||||
|
|
||||||
Ok(function)
|
Ok(function)
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,9 +65,9 @@ async fn net_request(lua: &Lua, config: RequestConfig) -> LuaResult<LuaTable> {
|
||||||
res.await?.into_lua_table(lua)
|
res.await?.into_lua_table(lua)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn net_socket(lua: &Lua, url: String) -> LuaResult<LuaTable> {
|
async fn net_socket(lua: &Lua, url: String) -> LuaResult<LuaValue> {
|
||||||
let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?;
|
let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?;
|
||||||
NetWebSocket::new(ws).into_lua_table(lua)
|
NetWebSocket::new(ws).into_lua(lua)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn net_serve<'lua>(
|
async fn net_serve<'lua>(
|
||||||
|
|
|
@ -40,13 +40,13 @@ impl Service<Request<Incoming>> for Svc {
|
||||||
lua.spawn_local(async move {
|
lua.spawn_local(async move {
|
||||||
let sock = sock.await.unwrap();
|
let sock = sock.await.unwrap();
|
||||||
let lua_sock = NetWebSocket::new(sock);
|
let lua_sock = NetWebSocket::new(sock);
|
||||||
let lua_tab = lua_sock.into_lua_table(&lua_inner).unwrap();
|
let lua_val = lua_sock.into_lua(&lua_inner).unwrap();
|
||||||
|
|
||||||
let handler_websocket: LuaFunction =
|
let handler_websocket: LuaFunction =
|
||||||
keys.websocket_handler(&lua_inner).unwrap().unwrap();
|
keys.websocket_handler(&lua_inner).unwrap().unwrap();
|
||||||
|
|
||||||
lua_inner
|
lua_inner
|
||||||
.push_thread_back(handler_websocket, lua_tab)
|
.push_thread_back(handler_websocket, lua_val)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -23,29 +23,6 @@ use hyper_tungstenite::{
|
||||||
WebSocketStream,
|
WebSocketStream,
|
||||||
};
|
};
|
||||||
|
|
||||||
use lune_utils::TableBuilder;
|
|
||||||
|
|
||||||
// Wrapper implementation for compatibility and changing colon syntax to dot syntax
|
|
||||||
const WEB_SOCKET_IMPL_LUA: &str = r#"
|
|
||||||
return freeze(setmetatable({
|
|
||||||
close = function(...)
|
|
||||||
return websocket:close(...)
|
|
||||||
end,
|
|
||||||
send = function(...)
|
|
||||||
return websocket:send(...)
|
|
||||||
end,
|
|
||||||
next = function(...)
|
|
||||||
return websocket:next(...)
|
|
||||||
end,
|
|
||||||
}, {
|
|
||||||
__index = function(self, key)
|
|
||||||
if key == "closeCode" then
|
|
||||||
return websocket.closeCode
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
}))
|
|
||||||
"#;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct NetWebSocket<T> {
|
pub struct NetWebSocket<T> {
|
||||||
close_code_exists: Arc<AtomicBool>,
|
close_code_exists: Arc<AtomicBool>,
|
||||||
|
@ -125,25 +102,6 @@ where
|
||||||
let mut ws = self.write_stream.lock().await;
|
let mut ws = self.write_stream.lock().await;
|
||||||
ws.close().await.into_lua_err()
|
ws.close().await.into_lua_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_lua_table(self, lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let setmetatable = lua.globals().get::<_, LuaFunction>("setmetatable")?;
|
|
||||||
let table_freeze = lua
|
|
||||||
.globals()
|
|
||||||
.get::<_, LuaTable>("table")?
|
|
||||||
.get::<_, LuaFunction>("freeze")?;
|
|
||||||
|
|
||||||
let env = TableBuilder::new(lua)?
|
|
||||||
.with_value("websocket", self.clone())?
|
|
||||||
.with_value("setmetatable", setmetatable)?
|
|
||||||
.with_value("freeze", table_freeze)?
|
|
||||||
.build_readonly()?;
|
|
||||||
|
|
||||||
lua.load(WEB_SOCKET_IMPL_LUA)
|
|
||||||
.set_name("websocket")
|
|
||||||
.set_environment(env)
|
|
||||||
.eval()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> LuaUserData for NetWebSocket<T>
|
impl<T> LuaUserData for NetWebSocket<T>
|
||||||
|
|
|
@ -20,6 +20,9 @@ directories = "5.0"
|
||||||
pin-project = "1.0"
|
pin-project = "1.0"
|
||||||
os_str_bytes = { version = "7.0", features = ["conversions"] }
|
os_str_bytes = { version = "7.0", features = ["conversions"] }
|
||||||
|
|
||||||
|
bstr = "1.9"
|
||||||
|
bytes = "1.6.0"
|
||||||
|
|
||||||
tokio = { version = "1", default-features = false, features = [
|
tokio = { version = "1", default-features = false, features = [
|
||||||
"io-std",
|
"io-std",
|
||||||
"io-util",
|
"io-util",
|
||||||
|
|
|
@ -1,27 +1,33 @@
|
||||||
#![allow(clippy::cargo_common_metadata)]
|
#![allow(clippy::cargo_common_metadata)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
env::{
|
env::{
|
||||||
self,
|
self,
|
||||||
consts::{ARCH, OS},
|
consts::{ARCH, OS},
|
||||||
},
|
},
|
||||||
path::MAIN_SEPARATOR,
|
path::MAIN_SEPARATOR,
|
||||||
process::Stdio,
|
process::Stdio,
|
||||||
|
rc::Rc,
|
||||||
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
|
|
||||||
use lune_utils::TableBuilder;
|
use lune_utils::TableBuilder;
|
||||||
use mlua_luau_scheduler::{Functions, LuaSpawnExt};
|
use mlua_luau_scheduler::{Functions, LuaSpawnExt};
|
||||||
|
use options::ProcessSpawnOptionsStdio;
|
||||||
use os_str_bytes::RawOsString;
|
use os_str_bytes::RawOsString;
|
||||||
use tokio::io::AsyncWriteExt;
|
use stream::{ChildProcessReader, ChildProcessWriter};
|
||||||
|
use tokio::{io::AsyncWriteExt, process::Child, sync::RwLock};
|
||||||
|
|
||||||
mod options;
|
mod options;
|
||||||
|
mod stream;
|
||||||
mod tee_writer;
|
mod tee_writer;
|
||||||
mod wait_for_child;
|
mod wait_for_child;
|
||||||
|
|
||||||
use self::options::ProcessSpawnOptions;
|
use self::options::ProcessSpawnOptions;
|
||||||
use self::wait_for_child::{wait_for_child, WaitForChildResult};
|
use self::wait_for_child::wait_for_child;
|
||||||
|
|
||||||
use lune_utils::path::get_current_dir;
|
use lune_utils::path::get_current_dir;
|
||||||
|
|
||||||
|
@ -44,6 +50,11 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
||||||
// Create constants for OS & processor architecture
|
// Create constants for OS & processor architecture
|
||||||
let os = lua.create_string(OS.to_lowercase())?;
|
let os = lua.create_string(OS.to_lowercase())?;
|
||||||
let arch = lua.create_string(ARCH.to_lowercase())?;
|
let arch = lua.create_string(ARCH.to_lowercase())?;
|
||||||
|
let endianness = lua.create_string(if cfg!(target_endian = "big") {
|
||||||
|
"big"
|
||||||
|
} else {
|
||||||
|
"little"
|
||||||
|
})?;
|
||||||
// Create readonly args array
|
// Create readonly args array
|
||||||
let args_vec = lua
|
let args_vec = lua
|
||||||
.app_data_ref::<Vec<String>>()
|
.app_data_ref::<Vec<String>>()
|
||||||
|
@ -69,11 +80,13 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
||||||
TableBuilder::new(lua)?
|
TableBuilder::new(lua)?
|
||||||
.with_value("os", os)?
|
.with_value("os", os)?
|
||||||
.with_value("arch", arch)?
|
.with_value("arch", arch)?
|
||||||
|
.with_value("endianness", endianness)?
|
||||||
.with_value("args", args_tab)?
|
.with_value("args", args_tab)?
|
||||||
.with_value("cwd", cwd_str)?
|
.with_value("cwd", cwd_str)?
|
||||||
.with_value("env", env_tab)?
|
.with_value("env", env_tab)?
|
||||||
.with_value("exit", process_exit)?
|
.with_value("exit", process_exit)?
|
||||||
.with_async_function("spawn", process_spawn)?
|
.with_async_function("exec", process_exec)?
|
||||||
|
.with_function("create", process_create)?
|
||||||
.build_readonly()
|
.build_readonly()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,11 +154,16 @@ fn process_env_iter<'lua>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn process_spawn(
|
async fn process_exec(
|
||||||
lua: &Lua,
|
lua: &Lua,
|
||||||
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
||||||
) -> LuaResult<LuaTable> {
|
) -> LuaResult<LuaTable> {
|
||||||
let res = lua.spawn(spawn_command(program, args, options)).await?;
|
let res = lua
|
||||||
|
.spawn(async move {
|
||||||
|
let cmd = spawn_command_with_stdin(program, args, options.clone()).await?;
|
||||||
|
wait_for_child(cmd, options.stdio.stdout, options.stdio.stderr).await
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
NOTE: If an exit code was not given by the child process,
|
NOTE: If an exit code was not given by the child process,
|
||||||
|
@ -168,30 +186,104 @@ async fn process_spawn(
|
||||||
.build_readonly()
|
.build_readonly()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn spawn_command(
|
#[allow(clippy::await_holding_refcell_ref)]
|
||||||
|
fn process_create(
|
||||||
|
lua: &Lua,
|
||||||
|
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
||||||
|
) -> LuaResult<LuaTable> {
|
||||||
|
// We do not want the user to provide stdio options for process.create,
|
||||||
|
// so we reset the options, regardless of what the user provides us
|
||||||
|
let mut spawn_options = options.clone();
|
||||||
|
spawn_options.stdio = ProcessSpawnOptionsStdio::default();
|
||||||
|
|
||||||
|
let (code_tx, code_rx) = tokio::sync::broadcast::channel(4);
|
||||||
|
let code_rx_rc = Rc::new(RefCell::new(code_rx));
|
||||||
|
|
||||||
|
let child = spawn_command(program, args, spawn_options)?;
|
||||||
|
|
||||||
|
let child_arc = Arc::new(RwLock::new(child));
|
||||||
|
|
||||||
|
let child_arc_clone = Arc::clone(&child_arc);
|
||||||
|
let mut child_lock = tokio::task::block_in_place(|| child_arc_clone.blocking_write());
|
||||||
|
|
||||||
|
let stdin = child_lock.stdin.take().unwrap();
|
||||||
|
let stdout = child_lock.stdout.take().unwrap();
|
||||||
|
let stderr = child_lock.stderr.take().unwrap();
|
||||||
|
|
||||||
|
let child_arc_inner = Arc::clone(&child_arc);
|
||||||
|
|
||||||
|
// Spawn a background task to wait for the child to exit and send the exit code
|
||||||
|
let status_handle = tokio::spawn(async move {
|
||||||
|
let res = child_arc_inner.write().await.wait().await;
|
||||||
|
|
||||||
|
if let Ok(output) = res {
|
||||||
|
let code = output.code().unwrap_or_default();
|
||||||
|
|
||||||
|
code_tx
|
||||||
|
.send(code)
|
||||||
|
.expect("ExitCode receiver was unexpectedly dropped");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
TableBuilder::new(lua)?
|
||||||
|
.with_value("stdout", ChildProcessReader(stdout))?
|
||||||
|
.with_value("stderr", ChildProcessReader(stderr))?
|
||||||
|
.with_value("stdin", ChildProcessWriter(stdin))?
|
||||||
|
.with_async_function("kill", move |_, ()| {
|
||||||
|
// First, stop the status task so the RwLock is dropped
|
||||||
|
status_handle.abort();
|
||||||
|
let child_arc_clone = Arc::clone(&child_arc);
|
||||||
|
|
||||||
|
// Then get another RwLock to write to the child process and kill it
|
||||||
|
async move { Ok(child_arc_clone.write().await.kill().await?) }
|
||||||
|
})?
|
||||||
|
.with_async_function("status", move |lua, ()| {
|
||||||
|
let code_rx_rc_clone = Rc::clone(&code_rx_rc);
|
||||||
|
async move {
|
||||||
|
// Exit code of 9 corresponds to SIGKILL, which should be the only case where
|
||||||
|
// the receiver gets suddenly dropped
|
||||||
|
let code = code_rx_rc_clone.borrow_mut().recv().await.unwrap_or(9);
|
||||||
|
|
||||||
|
TableBuilder::new(lua)?
|
||||||
|
.with_value("code", code)?
|
||||||
|
.with_value("ok", code == 0)?
|
||||||
|
.build_readonly()
|
||||||
|
}
|
||||||
|
})?
|
||||||
|
.build_readonly()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spawn_command_with_stdin(
|
||||||
program: String,
|
program: String,
|
||||||
args: Option<Vec<String>>,
|
args: Option<Vec<String>>,
|
||||||
mut options: ProcessSpawnOptions,
|
mut options: ProcessSpawnOptions,
|
||||||
) -> LuaResult<WaitForChildResult> {
|
) -> LuaResult<Child> {
|
||||||
let stdout = options.stdio.stdout;
|
|
||||||
let stderr = options.stdio.stderr;
|
|
||||||
let stdin = options.stdio.stdin.take();
|
let stdin = options.stdio.stdin.take();
|
||||||
|
|
||||||
let mut child = options
|
let mut child = spawn_command(program, args, options)?;
|
||||||
.into_command(program, args)
|
|
||||||
.stdin(if stdin.is_some() {
|
|
||||||
Stdio::piped()
|
|
||||||
} else {
|
|
||||||
Stdio::null()
|
|
||||||
})
|
|
||||||
.stdout(stdout.as_stdio())
|
|
||||||
.stderr(stderr.as_stdio())
|
|
||||||
.spawn()?;
|
|
||||||
|
|
||||||
if let Some(stdin) = stdin {
|
if let Some(stdin) = stdin {
|
||||||
let mut child_stdin = child.stdin.take().unwrap();
|
let mut child_stdin = child.stdin.take().unwrap();
|
||||||
child_stdin.write_all(&stdin).await.into_lua_err()?;
|
child_stdin.write_all(&stdin).await.into_lua_err()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_for_child(child, stdout, stderr).await
|
Ok(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_command(
|
||||||
|
program: String,
|
||||||
|
args: Option<Vec<String>>,
|
||||||
|
options: ProcessSpawnOptions,
|
||||||
|
) -> LuaResult<Child> {
|
||||||
|
let stdout = options.stdio.stdout;
|
||||||
|
let stderr = options.stdio.stderr;
|
||||||
|
|
||||||
|
let child = options
|
||||||
|
.into_command(program, args)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(stdout.as_stdio())
|
||||||
|
.stderr(stderr.as_stdio())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
Ok(child)
|
||||||
}
|
}
|
||||||
|
|
58
crates/lune-std-process/src/stream.rs
Normal file
58
crates/lune-std-process/src/stream.rs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
use bstr::BString;
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use mlua::prelude::*;
|
||||||
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||||
|
|
||||||
|
const CHUNK_SIZE: usize = 8;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ChildProcessReader<R: AsyncRead>(pub R);
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ChildProcessWriter<W: AsyncWrite>(pub W);
|
||||||
|
|
||||||
|
impl<R: AsyncRead + Unpin> ChildProcessReader<R> {
|
||||||
|
pub async fn read(&mut self, chunk_size: Option<usize>) -> LuaResult<Vec<u8>> {
|
||||||
|
let mut buf = BytesMut::with_capacity(chunk_size.unwrap_or(CHUNK_SIZE));
|
||||||
|
self.0.read_buf(&mut buf).await?;
|
||||||
|
|
||||||
|
Ok(buf.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_to_end(&mut self) -> LuaResult<Vec<u8>> {
|
||||||
|
let mut buf = vec![];
|
||||||
|
self.0.read_to_end(&mut buf).await?;
|
||||||
|
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: AsyncRead + Unpin + 'static> LuaUserData for ChildProcessReader<R> {
|
||||||
|
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||||
|
methods.add_async_method_mut("read", |lua, this, chunk_size: Option<usize>| async move {
|
||||||
|
let buf = this.read(chunk_size).await?;
|
||||||
|
|
||||||
|
if buf.is_empty() {
|
||||||
|
return Ok(LuaValue::Nil);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(LuaValue::String(lua.create_string(buf)?))
|
||||||
|
});
|
||||||
|
|
||||||
|
methods.add_async_method_mut("readToEnd", |lua, this, ()| async {
|
||||||
|
Ok(lua.create_string(this.read_to_end().await?))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: AsyncWrite + Unpin> ChildProcessWriter<W> {
|
||||||
|
pub async fn write(&mut self, data: BString) -> LuaResult<()> {
|
||||||
|
self.0.write_all(data.as_ref()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: AsyncWrite + Unpin + 'static> LuaUserData for ChildProcessWriter<W> {
|
||||||
|
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||||
|
methods.add_async_method_mut("write", |_, this, data| async { this.write(data).await });
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,7 +33,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, W> AsyncWrite for AsyncTeeWriter<'a, W>
|
impl<W> AsyncWrite for AsyncTeeWriter<'_, W>
|
||||||
where
|
where
|
||||||
W: AsyncWrite + Unpin,
|
W: AsyncWrite + Unpin,
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "lune-std-roblox"
|
name = "lune-std-roblox"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
repository = "https://github.com/lune-org/lune"
|
repository = "https://github.com/lune-org/lune"
|
||||||
|
@ -18,6 +18,7 @@ mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
|
||||||
|
|
||||||
once_cell = "1.17"
|
once_cell = "1.17"
|
||||||
rbx_cookie = { version = "0.1.4", default-features = false }
|
rbx_cookie = { version = "0.1.4", default-features = false }
|
||||||
|
roblox_install = "1.0.0"
|
||||||
|
|
||||||
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
||||||
lune-roblox = { version = "0.1.3", path = "../lune-roblox" }
|
lune-roblox = { version = "0.1.4", path = "../lune-roblox" }
|
||||||
|
|
|
@ -13,6 +13,7 @@ use lune_roblox::{
|
||||||
static REFLECTION_DATABASE: OnceCell<ReflectionDatabase> = OnceCell::new();
|
static REFLECTION_DATABASE: OnceCell<ReflectionDatabase> = OnceCell::new();
|
||||||
|
|
||||||
use lune_utils::TableBuilder;
|
use lune_utils::TableBuilder;
|
||||||
|
use roblox_install::RobloxStudio;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Creates the `roblox` standard library module.
|
Creates the `roblox` standard library module.
|
||||||
|
@ -39,6 +40,10 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
||||||
.with_function("getReflectionDatabase", get_reflection_database)?
|
.with_function("getReflectionDatabase", get_reflection_database)?
|
||||||
.with_function("implementProperty", implement_property)?
|
.with_function("implementProperty", implement_property)?
|
||||||
.with_function("implementMethod", implement_method)?
|
.with_function("implementMethod", implement_method)?
|
||||||
|
.with_function("studioApplicationPath", studio_application_path)?
|
||||||
|
.with_function("studioContentPath", studio_content_path)?
|
||||||
|
.with_function("studioPluginPath", studio_plugin_path)?
|
||||||
|
.with_function("studioBuiltinPluginPath", studio_builtin_plugin_path)?
|
||||||
.build_readonly()
|
.build_readonly()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +77,7 @@ async fn serialize_place<'lua>(
|
||||||
lua: &'lua Lua,
|
lua: &'lua Lua,
|
||||||
(data_model, as_xml): (LuaUserDataRef<'lua, Instance>, Option<bool>),
|
(data_model, as_xml): (LuaUserDataRef<'lua, Instance>, Option<bool>),
|
||||||
) -> LuaResult<LuaString<'lua>> {
|
) -> LuaResult<LuaString<'lua>> {
|
||||||
let data_model = (*data_model).clone();
|
let data_model = *data_model;
|
||||||
let fut = lua.spawn_blocking(move || {
|
let fut = lua.spawn_blocking(move || {
|
||||||
let doc = Document::from_data_model_instance(data_model)?;
|
let doc = Document::from_data_model_instance(data_model)?;
|
||||||
let bytes = doc.to_bytes_with_format(match as_xml {
|
let bytes = doc.to_bytes_with_format(match as_xml {
|
||||||
|
@ -89,7 +94,7 @@ async fn serialize_model<'lua>(
|
||||||
lua: &'lua Lua,
|
lua: &'lua Lua,
|
||||||
(instances, as_xml): (Vec<LuaUserDataRef<'lua, Instance>>, Option<bool>),
|
(instances, as_xml): (Vec<LuaUserDataRef<'lua, Instance>>, Option<bool>),
|
||||||
) -> LuaResult<LuaString<'lua>> {
|
) -> LuaResult<LuaString<'lua>> {
|
||||||
let instances = instances.iter().map(|i| (*i).clone()).collect();
|
let instances = instances.iter().map(|i| **i).collect();
|
||||||
let fut = lua.spawn_blocking(move || {
|
let fut = lua.spawn_blocking(move || {
|
||||||
let doc = Document::from_instance_array(instances)?;
|
let doc = Document::from_instance_array(instances)?;
|
||||||
let bytes = doc.to_bytes_with_format(match as_xml {
|
let bytes = doc.to_bytes_with_format(match as_xml {
|
||||||
|
@ -147,3 +152,27 @@ fn implement_method(
|
||||||
InstanceRegistry::insert_method(lua, &class_name, &method_name, method).into_lua_err()?;
|
InstanceRegistry::insert_method(lua, &class_name, &method_name, method).into_lua_err()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn studio_application_path(_: &Lua, _: ()) -> LuaResult<String> {
|
||||||
|
RobloxStudio::locate()
|
||||||
|
.map(|rs| rs.application_path().display().to_string())
|
||||||
|
.map_err(LuaError::external)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn studio_content_path(_: &Lua, _: ()) -> LuaResult<String> {
|
||||||
|
RobloxStudio::locate()
|
||||||
|
.map(|rs| rs.content_path().display().to_string())
|
||||||
|
.map_err(LuaError::external)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn studio_plugin_path(_: &Lua, _: ()) -> LuaResult<String> {
|
||||||
|
RobloxStudio::locate()
|
||||||
|
.map(|rs| rs.plugins_path().display().to_string())
|
||||||
|
.map_err(LuaError::external)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn studio_builtin_plugin_path(_: &Lua, _: ()) -> LuaResult<String> {
|
||||||
|
RobloxStudio::locate()
|
||||||
|
.map(|rs| rs.built_in_plugins_path().display().to_string())
|
||||||
|
.map_err(LuaError::external)
|
||||||
|
}
|
||||||
|
|
|
@ -117,7 +117,7 @@ impl<'lua> FromLua<'lua> for CompressDecompressFormat {
|
||||||
|
|
||||||
Errors when the compression fails.
|
Errors when the compression fails.
|
||||||
*/
|
*/
|
||||||
pub async fn compress<'lua>(
|
pub async fn compress(
|
||||||
source: impl AsRef<[u8]>,
|
source: impl AsRef<[u8]>,
|
||||||
format: CompressDecompressFormat,
|
format: CompressDecompressFormat,
|
||||||
level: Option<i32>,
|
level: Option<i32>,
|
||||||
|
@ -163,7 +163,7 @@ pub async fn compress<'lua>(
|
||||||
|
|
||||||
Errors when the decompression fails.
|
Errors when the decompression fails.
|
||||||
*/
|
*/
|
||||||
pub async fn decompress<'lua>(
|
pub async fn decompress(
|
||||||
source: impl AsRef<[u8]>,
|
source: impl AsRef<[u8]>,
|
||||||
format: CompressDecompressFormat,
|
format: CompressDecompressFormat,
|
||||||
) -> LuaResult<Vec<u8>> {
|
) -> LuaResult<Vec<u8>> {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "lune-std"
|
name = "lune-std"
|
||||||
version = "0.1.4"
|
version = "0.1.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
repository = "https://github.com/lune-org/lune"
|
repository = "https://github.com/lune-org/lune"
|
||||||
|
@ -53,7 +53,7 @@ lune-std-luau = { optional = true, version = "0.1.2", path = "../lune-std-luau"
|
||||||
lune-std-net = { optional = true, version = "0.1.2", path = "../lune-std-net" }
|
lune-std-net = { optional = true, version = "0.1.2", path = "../lune-std-net" }
|
||||||
lune-std-process = { optional = true, version = "0.1.3", path = "../lune-std-process" }
|
lune-std-process = { optional = true, version = "0.1.3", path = "../lune-std-process" }
|
||||||
lune-std-regex = { optional = true, version = "0.1.2", path = "../lune-std-regex" }
|
lune-std-regex = { optional = true, version = "0.1.2", path = "../lune-std-regex" }
|
||||||
lune-std-roblox = { optional = true, version = "0.1.3", path = "../lune-std-roblox" }
|
lune-std-roblox = { optional = true, version = "0.1.4", path = "../lune-std-roblox" }
|
||||||
lune-std-serde = { optional = true, version = "0.1.2", path = "../lune-std-serde" }
|
lune-std-serde = { optional = true, version = "0.1.2", path = "../lune-std-serde" }
|
||||||
lune-std-stdio = { optional = true, version = "0.1.2", path = "../lune-std-stdio" }
|
lune-std-stdio = { optional = true, version = "0.1.2", path = "../lune-std-stdio" }
|
||||||
lune-std-task = { optional = true, version = "0.1.2", path = "../lune-std-task" }
|
lune-std-task = { optional = true, version = "0.1.2", path = "../lune-std-task" }
|
||||||
|
|
|
@ -150,9 +150,9 @@ impl RequireContext {
|
||||||
self.get_from_cache(lua, abs_path.as_ref())
|
self.get_from_cache(lua, abs_path.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load<'lua>(
|
async fn load(
|
||||||
&self,
|
&self,
|
||||||
lua: &'lua Lua,
|
lua: &Lua,
|
||||||
abs_path: impl AsRef<Path>,
|
abs_path: impl AsRef<Path>,
|
||||||
rel_path: impl AsRef<Path>,
|
rel_path: impl AsRef<Path>,
|
||||||
) -> LuaResult<LuaRegistryKey> {
|
) -> LuaResult<LuaRegistryKey> {
|
||||||
|
|
|
@ -22,3 +22,5 @@ dunce = "1.0"
|
||||||
once_cell = "1.17"
|
once_cell = "1.17"
|
||||||
path-clean = "1.0"
|
path-clean = "1.0"
|
||||||
pathdiff = "0.2"
|
pathdiff = "0.2"
|
||||||
|
parking_lot = "0.12.3"
|
||||||
|
semver = "1.0"
|
||||||
|
|
|
@ -26,6 +26,11 @@ static STYLED_STACK_END: Lazy<String> = Lazy::new(|| {
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NOTE: We indent using 4 spaces instead of tabs since
|
||||||
|
// these errors are most likely to be displayed in a terminal
|
||||||
|
// or some kind of live output - and tabs don't work well there
|
||||||
|
const STACK_TRACE_INDENT: &str = " ";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Error components parsed from a [`LuaError`].
|
Error components parsed from a [`LuaError`].
|
||||||
|
|
||||||
|
@ -86,7 +91,7 @@ impl fmt::Display for ErrorComponents {
|
||||||
let trace = self.trace.as_ref().unwrap();
|
let trace = self.trace.as_ref().unwrap();
|
||||||
writeln!(f, "{}", *STYLED_STACK_BEGIN)?;
|
writeln!(f, "{}", *STYLED_STACK_BEGIN)?;
|
||||||
for line in trace.lines() {
|
for line in trace.lines() {
|
||||||
writeln!(f, "\t{line}")?;
|
writeln!(f, "{STACK_TRACE_INDENT}{line}")?;
|
||||||
}
|
}
|
||||||
writeln!(f, "{}", *STYLED_STACK_END)?;
|
writeln!(f, "{}", *STYLED_STACK_END)?;
|
||||||
}
|
}
|
||||||
|
@ -124,7 +129,7 @@ impl From<LuaError> for ErrorComponents {
|
||||||
}
|
}
|
||||||
|
|
||||||
// We will then try to extract any stack trace
|
// We will then try to extract any stack trace
|
||||||
let trace = if let LuaError::CallbackError {
|
let mut trace = if let LuaError::CallbackError {
|
||||||
ref traceback,
|
ref traceback,
|
||||||
ref cause,
|
ref cause,
|
||||||
} = *error
|
} = *error
|
||||||
|
@ -147,6 +152,45 @@ impl From<LuaError> for ErrorComponents {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sometimes, we can get duplicate stack trace lines that only
|
||||||
|
// mention "[C]", without a function name or path, and these can
|
||||||
|
// be safely ignored / removed if the following line has more info
|
||||||
|
if let Some(trace) = &mut trace {
|
||||||
|
let lines = trace.lines_mut();
|
||||||
|
loop {
|
||||||
|
let first_is_c_and_empty = lines
|
||||||
|
.first()
|
||||||
|
.is_some_and(|line| line.source().is_c() && line.is_empty());
|
||||||
|
let second_is_c_and_nonempty = lines
|
||||||
|
.get(1)
|
||||||
|
.is_some_and(|line| line.source().is_c() && !line.is_empty());
|
||||||
|
if first_is_c_and_empty && second_is_c_and_nonempty {
|
||||||
|
lines.remove(0);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we do some light postprocessing to remove duplicate
|
||||||
|
// information, such as the location prefix in the error message
|
||||||
|
if let Some(message) = messages.last_mut() {
|
||||||
|
if let Some(line) = trace
|
||||||
|
.iter()
|
||||||
|
.flat_map(StackTrace::lines)
|
||||||
|
.find(|line| line.source().is_lua())
|
||||||
|
{
|
||||||
|
let location_prefix = format!(
|
||||||
|
"[string \"{}\"]:{}:",
|
||||||
|
line.path().unwrap(),
|
||||||
|
line.line_number().unwrap()
|
||||||
|
);
|
||||||
|
if message.starts_with(&location_prefix) {
|
||||||
|
*message = message[location_prefix.len()..].trim().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ErrorComponents { messages, trace }
|
ErrorComponents { messages, trace }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,24 @@ pub enum StackTraceSource {
|
||||||
Lua,
|
Lua,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StackTraceSource {
|
||||||
|
/**
|
||||||
|
Returns `true` if the error originated from a C / Rust function, `false` otherwise.
|
||||||
|
*/
|
||||||
|
#[must_use]
|
||||||
|
pub const fn is_c(self) -> bool {
|
||||||
|
matches!(self, Self::C)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Returns `true` if the error originated from a Lua (user) function, `false` otherwise.
|
||||||
|
*/
|
||||||
|
#[must_use]
|
||||||
|
pub const fn is_lua(self) -> bool {
|
||||||
|
matches!(self, Self::Lua)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Stack trace line parsed from a [`LuaError`].
|
Stack trace line parsed from a [`LuaError`].
|
||||||
*/
|
*/
|
||||||
|
@ -82,6 +100,20 @@ impl StackTraceLine {
|
||||||
pub fn function_name(&self) -> Option<&str> {
|
pub fn function_name(&self) -> Option<&str> {
|
||||||
self.function_name.as_deref()
|
self.function_name.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Returns `true` if the stack trace line contains no "useful" information, `false` otherwise.
|
||||||
|
|
||||||
|
Useful information is determined as one of:
|
||||||
|
|
||||||
|
- A path
|
||||||
|
- A line number
|
||||||
|
- A function name
|
||||||
|
*/
|
||||||
|
#[must_use]
|
||||||
|
pub const fn is_empty(&self) -> bool {
|
||||||
|
self.path.is_none() && self.line_number.is_none() && self.function_name.is_none()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for StackTraceLine {
|
impl FromStr for StackTraceLine {
|
||||||
|
@ -145,6 +177,14 @@ impl StackTrace {
|
||||||
pub fn lines(&self) -> &[StackTraceLine] {
|
pub fn lines(&self) -> &[StackTraceLine] {
|
||||||
&self.lines
|
&self.lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Returns the individual stack trace lines, mutably.
|
||||||
|
*/
|
||||||
|
#[must_use]
|
||||||
|
pub fn lines_mut(&mut self) -> &mut Vec<StackTraceLine> {
|
||||||
|
&mut self.lines
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for StackTrace {
|
impl FromStr for StackTrace {
|
||||||
|
|
|
@ -2,7 +2,7 @@ use mlua::prelude::*;
|
||||||
|
|
||||||
use crate::fmt::ErrorComponents;
|
use crate::fmt::ErrorComponents;
|
||||||
|
|
||||||
fn new_lua_result() -> LuaResult<()> {
|
fn new_lua_runtime_error() -> LuaResult<()> {
|
||||||
let lua = Lua::new();
|
let lua = Lua::new();
|
||||||
|
|
||||||
lua.globals()
|
lua.globals()
|
||||||
|
@ -17,13 +17,34 @@ fn new_lua_result() -> LuaResult<()> {
|
||||||
lua.load("f()").set_name("chunk_name").eval()
|
lua.load("f()").set_name("chunk_name").eval()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn new_lua_script_error() -> LuaResult<()> {
|
||||||
|
let lua = Lua::new();
|
||||||
|
|
||||||
|
lua.load(
|
||||||
|
"local function inner()\
|
||||||
|
\n error(\"oh no, a script error\")\
|
||||||
|
\nend\
|
||||||
|
\n\
|
||||||
|
\nlocal function outer()\
|
||||||
|
\n inner()\
|
||||||
|
\nend\
|
||||||
|
\n\
|
||||||
|
\nouter()\
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.set_name("chunk_name")
|
||||||
|
.eval()
|
||||||
|
}
|
||||||
|
|
||||||
// Tests for error context stack
|
// Tests for error context stack
|
||||||
mod context {
|
mod context {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn preserves_original() {
|
fn preserves_original() {
|
||||||
let lua_error = new_lua_result().context("additional context").unwrap_err();
|
let lua_error = new_lua_runtime_error()
|
||||||
|
.context("additional context")
|
||||||
|
.unwrap_err();
|
||||||
let components = ErrorComponents::from(lua_error);
|
let components = ErrorComponents::from(lua_error);
|
||||||
|
|
||||||
assert_eq!(components.messages()[0], "additional context");
|
assert_eq!(components.messages()[0], "additional context");
|
||||||
|
@ -34,7 +55,7 @@ mod context {
|
||||||
fn preserves_levels() {
|
fn preserves_levels() {
|
||||||
// NOTE: The behavior in mlua is to preserve a single level of context
|
// NOTE: The behavior in mlua is to preserve a single level of context
|
||||||
// and not all levels (context gets replaced on each call to `context`)
|
// and not all levels (context gets replaced on each call to `context`)
|
||||||
let lua_error = new_lua_result()
|
let lua_error = new_lua_runtime_error()
|
||||||
.context("level 1")
|
.context("level 1")
|
||||||
.context("level 2")
|
.context("level 2")
|
||||||
.context("level 3")
|
.context("level 3")
|
||||||
|
@ -54,7 +75,7 @@ mod error_components {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn message() {
|
fn message() {
|
||||||
let lua_error = new_lua_result().unwrap_err();
|
let lua_error = new_lua_runtime_error().unwrap_err();
|
||||||
let components = ErrorComponents::from(lua_error);
|
let components = ErrorComponents::from(lua_error);
|
||||||
|
|
||||||
assert_eq!(components.messages()[0], "oh no, a runtime error");
|
assert_eq!(components.messages()[0], "oh no, a runtime error");
|
||||||
|
@ -62,7 +83,7 @@ mod error_components {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stack_begin_end() {
|
fn stack_begin_end() {
|
||||||
let lua_error = new_lua_result().unwrap_err();
|
let lua_error = new_lua_runtime_error().unwrap_err();
|
||||||
let formatted = format!("{}", ErrorComponents::from(lua_error));
|
let formatted = format!("{}", ErrorComponents::from(lua_error));
|
||||||
|
|
||||||
assert!(formatted.contains("Stack Begin"));
|
assert!(formatted.contains("Stack Begin"));
|
||||||
|
@ -71,7 +92,7 @@ mod error_components {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stack_lines() {
|
fn stack_lines() {
|
||||||
let lua_error = new_lua_result().unwrap_err();
|
let lua_error = new_lua_runtime_error().unwrap_err();
|
||||||
let components = ErrorComponents::from(lua_error);
|
let components = ErrorComponents::from(lua_error);
|
||||||
|
|
||||||
let mut lines = components.trace().unwrap().lines().iter();
|
let mut lines = components.trace().unwrap().lines().iter();
|
||||||
|
@ -83,3 +104,47 @@ mod error_components {
|
||||||
assert_eq!(line_2, "Script 'chunk_name', Line 1");
|
assert_eq!(line_2, "Script 'chunk_name', Line 1");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests for general formatting
|
||||||
|
mod general {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn message_does_not_contain_location() {
|
||||||
|
let lua_error = new_lua_script_error().unwrap_err();
|
||||||
|
|
||||||
|
let components = ErrorComponents::from(lua_error);
|
||||||
|
let trace = components.trace().unwrap();
|
||||||
|
|
||||||
|
let first_message = components.messages().first().unwrap();
|
||||||
|
let first_lua_stack_line = trace
|
||||||
|
.lines()
|
||||||
|
.iter()
|
||||||
|
.find(|line| line.source().is_lua())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let location_prefix = format!(
|
||||||
|
"[string \"{}\"]:{}:",
|
||||||
|
first_lua_stack_line.path().unwrap(),
|
||||||
|
first_lua_stack_line.line_number().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(!first_message.starts_with(&location_prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_redundant_c_mentions() {
|
||||||
|
let lua_error = new_lua_script_error().unwrap_err();
|
||||||
|
|
||||||
|
let components = ErrorComponents::from(lua_error);
|
||||||
|
let trace = components.trace().unwrap();
|
||||||
|
|
||||||
|
let c_stack_lines = trace
|
||||||
|
.lines()
|
||||||
|
.iter()
|
||||||
|
.filter(|line| line.source().is_c())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(c_stack_lines.len(), 1); // Just the "error" call
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
use std::{
|
use std::{collections::HashSet, sync::Arc};
|
||||||
collections::HashSet,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use console::{colors_enabled as get_colors_enabled, set_colors_enabled};
|
use console::{colors_enabled as get_colors_enabled, set_colors_enabled};
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use parking_lot::ReentrantMutex;
|
||||||
|
|
||||||
mod basic;
|
mod basic;
|
||||||
mod config;
|
mod config;
|
||||||
|
@ -20,7 +18,7 @@ pub use self::config::ValueFormatConfig;
|
||||||
// NOTE: Since the setting for colors being enabled is global,
|
// NOTE: Since the setting for colors being enabled is global,
|
||||||
// and these functions may be called in parallel, we use this global
|
// and these functions may be called in parallel, we use this global
|
||||||
// lock to make sure that we don't mess up the colors for other threads.
|
// lock to make sure that we don't mess up the colors for other threads.
|
||||||
static COLORS_LOCK: Lazy<Arc<Mutex<()>>> = Lazy::new(|| Arc::new(Mutex::new(())));
|
static COLORS_LOCK: Lazy<Arc<ReentrantMutex<()>>> = Lazy::new(|| Arc::new(ReentrantMutex::new(())));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Formats a Lua value into a pretty string using the given config.
|
Formats a Lua value into a pretty string using the given config.
|
||||||
|
@ -28,7 +26,7 @@ static COLORS_LOCK: Lazy<Arc<Mutex<()>>> = Lazy::new(|| Arc::new(Mutex::new(()))
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[allow(clippy::missing_panics_doc)]
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn pretty_format_value(value: &LuaValue, config: &ValueFormatConfig) -> String {
|
pub fn pretty_format_value(value: &LuaValue, config: &ValueFormatConfig) -> String {
|
||||||
let _guard = COLORS_LOCK.lock().unwrap();
|
let _guard = COLORS_LOCK.lock();
|
||||||
|
|
||||||
let were_colors_enabled = get_colors_enabled();
|
let were_colors_enabled = get_colors_enabled();
|
||||||
set_colors_enabled(were_colors_enabled && config.colors_enabled);
|
set_colors_enabled(were_colors_enabled && config.colors_enabled);
|
||||||
|
@ -48,7 +46,7 @@ pub fn pretty_format_value(value: &LuaValue, config: &ValueFormatConfig) -> Stri
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[allow(clippy::missing_panics_doc)]
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn pretty_format_multi_value(values: &LuaMultiValue, config: &ValueFormatConfig) -> String {
|
pub fn pretty_format_multi_value(values: &LuaMultiValue, config: &ValueFormatConfig) -> String {
|
||||||
let _guard = COLORS_LOCK.lock().unwrap();
|
let _guard = COLORS_LOCK.lock();
|
||||||
|
|
||||||
let were_colors_enabled = get_colors_enabled();
|
let were_colors_enabled = get_colors_enabled();
|
||||||
set_colors_enabled(were_colors_enabled && config.colors_enabled);
|
set_colors_enabled(were_colors_enabled && config.colors_enabled);
|
||||||
|
|
30
crates/lune-utils/src/jit.rs
Normal file
30
crates/lune-utils/src/jit.rs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct JitStatus(bool);
|
||||||
|
|
||||||
|
impl JitStatus {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(enabled: bool) -> Self {
|
||||||
|
Self(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_status(&mut self, enabled: bool) {
|
||||||
|
self.0 = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn enabled(self) -> bool {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<JitStatus> for bool {
|
||||||
|
fn from(val: JitStatus) -> Self {
|
||||||
|
val.enabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bool> for JitStatus {
|
||||||
|
fn from(val: bool) -> Self {
|
||||||
|
Self::new(val)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ mod table_builder;
|
||||||
mod version_string;
|
mod version_string;
|
||||||
|
|
||||||
pub mod fmt;
|
pub mod fmt;
|
||||||
|
pub mod jit;
|
||||||
pub mod path;
|
pub mod path;
|
||||||
|
|
||||||
pub use self::table_builder::TableBuilder;
|
pub use self::table_builder::TableBuilder;
|
||||||
|
|
|
@ -2,6 +2,7 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use semver::Version;
|
||||||
|
|
||||||
static LUAU_VERSION: Lazy<Arc<String>> = Lazy::new(create_luau_version_string);
|
static LUAU_VERSION: Lazy<Arc<String>> = Lazy::new(create_luau_version_string);
|
||||||
|
|
||||||
|
@ -20,12 +21,10 @@ pub fn get_version_string(lune_version: impl AsRef<str>) -> String {
|
||||||
let lune_version = lune_version.as_ref();
|
let lune_version = lune_version.as_ref();
|
||||||
|
|
||||||
assert!(!lune_version.is_empty(), "Lune version string is empty");
|
assert!(!lune_version.is_empty(), "Lune version string is empty");
|
||||||
assert!(
|
match Version::parse(lune_version) {
|
||||||
lune_version.chars().all(is_valid_version_char),
|
Ok(semver) => format!("Lune {semver}+{}", *LUAU_VERSION),
|
||||||
"Lune version string contains invalid characters"
|
Err(e) => panic!("Lune version string is not valid semver: {e}"),
|
||||||
);
|
}
|
||||||
|
|
||||||
format!("Lune {lune_version}+{}", *LUAU_VERSION)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_luau_version_string() -> Arc<String> {
|
fn create_luau_version_string() -> Arc<String> {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "lune"
|
name = "lune"
|
||||||
version = "0.8.7"
|
version = "0.8.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
repository = "https://github.com/lune-org/lune"
|
repository = "https://github.com/lune-org/lune"
|
||||||
|
@ -71,8 +71,8 @@ reqwest = { version = "0.11", default-features = false, features = [
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
lune-std = { optional = true, version = "0.1.4", path = "../lune-std" }
|
lune-std = { optional = true, version = "0.1.5", path = "../lune-std" }
|
||||||
lune-roblox = { optional = true, version = "0.1.3", path = "../lune-roblox" }
|
lune-roblox = { optional = true, version = "0.1.4", path = "../lune-roblox" }
|
||||||
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
||||||
|
|
||||||
### CLI
|
### CLI
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::process::ExitCode;
|
use std::{env, process::ExitCode};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
@ -40,17 +40,27 @@ impl RunCommand {
|
||||||
(file_display_name, file_contents)
|
(file_display_name, file_contents)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a new lune object with all globals & run the script
|
// Create a new lune runtime with all globals & run the script
|
||||||
let result = Runtime::new()
|
let mut rt = Runtime::new()
|
||||||
.with_args(self.script_args)
|
.with_args(self.script_args)
|
||||||
|
// Enable JIT compilation unless it was requested to be disabled
|
||||||
|
.with_jit(
|
||||||
|
!matches!(
|
||||||
|
env::var("LUNE_LUAU_JIT").ok(),
|
||||||
|
Some(jit_enabled) if jit_enabled == "0" || jit_enabled == "false" || jit_enabled == "off"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = rt
|
||||||
.run(&script_display_name, strip_shebang(script_contents))
|
.run(&script_display_name, strip_shebang(script_contents))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
Ok(match result {
|
Ok(match result {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("{err}");
|
eprintln!("{err}");
|
||||||
ExitCode::FAILURE
|
ExitCode::FAILURE
|
||||||
}
|
}
|
||||||
Ok(code) => code,
|
Ok((code, _)) => ExitCode::from(code),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,8 +64,8 @@ pub fn discover_script_path(path: impl AsRef<str>, in_home_dir: bool) -> Result<
|
||||||
// NOTE: We use metadata directly here to try to
|
// NOTE: We use metadata directly here to try to
|
||||||
// avoid accessing the file path more than once
|
// avoid accessing the file path more than once
|
||||||
let file_meta = file_path.metadata();
|
let file_meta = file_path.metadata();
|
||||||
let is_file = file_meta.as_ref().map_or(false, Metadata::is_file);
|
let is_file = file_meta.as_ref().is_ok_and(Metadata::is_file);
|
||||||
let is_dir = file_meta.as_ref().map_or(false, Metadata::is_dir);
|
let is_dir = file_meta.as_ref().is_ok_and(Metadata::is_dir);
|
||||||
let is_abs = file_path.is_absolute();
|
let is_abs = file_path.is_absolute();
|
||||||
let ext = file_path.extension();
|
let ext = file_path.extension();
|
||||||
if is_file {
|
if is_file {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#![allow(clippy::missing_panics_doc)]
|
#![allow(clippy::missing_panics_doc)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
process::ExitCode,
|
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
|
@ -9,6 +8,7 @@ use std::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use lune_utils::jit::JitStatus;
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
use mlua_luau_scheduler::{Functions, Scheduler};
|
use mlua_luau_scheduler::{Functions, Scheduler};
|
||||||
use self_cell::self_cell;
|
use self_cell::self_cell;
|
||||||
|
@ -101,6 +101,7 @@ impl RuntimeInner {
|
||||||
*/
|
*/
|
||||||
pub struct Runtime {
|
pub struct Runtime {
|
||||||
inner: RuntimeInner,
|
inner: RuntimeInner,
|
||||||
|
jit_status: JitStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Runtime {
|
impl Runtime {
|
||||||
|
@ -114,6 +115,7 @@ impl Runtime {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: RuntimeInner::create().expect("Failed to create runtime"),
|
inner: RuntimeInner::create().expect("Failed to create runtime"),
|
||||||
|
jit_status: JitStatus::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,6 +133,15 @@ impl Runtime {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Enables or disables JIT compilation.
|
||||||
|
*/
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_jit(mut self, jit_status: impl Into<JitStatus>) -> Self {
|
||||||
|
self.jit_status = jit_status.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Runs a Lune script inside of the current runtime.
|
Runs a Lune script inside of the current runtime.
|
||||||
|
|
||||||
|
@ -144,7 +155,7 @@ impl Runtime {
|
||||||
&mut self,
|
&mut self,
|
||||||
script_name: impl AsRef<str>,
|
script_name: impl AsRef<str>,
|
||||||
script_contents: impl AsRef<[u8]>,
|
script_contents: impl AsRef<[u8]>,
|
||||||
) -> RuntimeResult<ExitCode> {
|
) -> RuntimeResult<(u8, Vec<LuaValue>)> {
|
||||||
let lua = self.inner.lua();
|
let lua = self.inner.lua();
|
||||||
let sched = self.inner.scheduler();
|
let sched = self.inner.scheduler();
|
||||||
|
|
||||||
|
@ -156,24 +167,29 @@ impl Runtime {
|
||||||
eprintln!("{}", RuntimeError::from(e));
|
eprintln!("{}", RuntimeError::from(e));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Enable / disable the JIT as requested and store the current status as AppData
|
||||||
|
lua.set_app_data(self.jit_status);
|
||||||
|
lua.enable_jit(self.jit_status.enabled());
|
||||||
|
|
||||||
// Load our "main" thread
|
// Load our "main" thread
|
||||||
let main = lua
|
let main = lua
|
||||||
.load(script_contents.as_ref())
|
.load(script_contents.as_ref())
|
||||||
.set_name(script_name.as_ref());
|
.set_name(script_name.as_ref());
|
||||||
|
|
||||||
// Run it on our scheduler until it and any other spawned threads complete
|
// Run it on our scheduler until it and any other spawned threads complete
|
||||||
sched.push_thread_back(main, ())?;
|
let main_thread_id = sched.push_thread_back(main, ())?;
|
||||||
sched.run().await;
|
sched.run().await;
|
||||||
|
|
||||||
// Return the exit code - default to FAILURE if we got any errors
|
let main_thread_res = match sched.get_thread_result(main_thread_id) {
|
||||||
let exit_code = sched.get_exit_code().unwrap_or({
|
Some(res) => res,
|
||||||
if got_any_error.load(Ordering::SeqCst) {
|
None => LuaValue::Nil.into_lua_multi(lua),
|
||||||
ExitCode::FAILURE
|
}?;
|
||||||
} else {
|
|
||||||
ExitCode::SUCCESS
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(exit_code)
|
Ok((
|
||||||
|
sched
|
||||||
|
.get_exit_code()
|
||||||
|
.unwrap_or(u8::from(got_any_error.load(Ordering::SeqCst))),
|
||||||
|
main_thread_res.into_vec(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,16 +29,15 @@ pub async fn run(patched_bin: impl AsRef<[u8]>) -> Result<ExitCode> {
|
||||||
let args = env::args().skip(1).collect::<Vec<_>>();
|
let args = env::args().skip(1).collect::<Vec<_>>();
|
||||||
let meta = Metadata::from_bytes(patched_bin).expect("must be a standalone binary");
|
let meta = Metadata::from_bytes(patched_bin).expect("must be a standalone binary");
|
||||||
|
|
||||||
let result = Runtime::new()
|
let mut rt = Runtime::new().with_args(args);
|
||||||
.with_args(args)
|
|
||||||
.run("STANDALONE", meta.bytecode)
|
let result = rt.run("STANDALONE", meta.bytecode).await;
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(match result {
|
Ok(match result {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("{err}");
|
eprintln!("{err}");
|
||||||
ExitCode::FAILURE
|
ExitCode::FAILURE
|
||||||
}
|
}
|
||||||
Ok(code) => code,
|
Ok((code, _)) => ExitCode::from(code),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,19 +31,21 @@ macro_rules! create_tests {
|
||||||
// The rest of the test logic can continue as normal
|
// The rest of the test logic can continue as normal
|
||||||
let full_name = format!("{}/tests/{}.luau", workspace_dir.display(), $value);
|
let full_name = format!("{}/tests/{}.luau", workspace_dir.display(), $value);
|
||||||
let script = read_to_string(&full_name).await?;
|
let script = read_to_string(&full_name).await?;
|
||||||
let mut lune = Runtime::new().with_args(
|
let mut lune = Runtime::new()
|
||||||
ARGS
|
.with_jit(true)
|
||||||
.clone()
|
.with_args(
|
||||||
.iter()
|
ARGS
|
||||||
.map(ToString::to_string)
|
.clone()
|
||||||
.collect::<Vec<_>>()
|
.iter()
|
||||||
);
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
let script_name = full_name
|
let script_name = full_name
|
||||||
.trim_end_matches(".luau")
|
.trim_end_matches(".luau")
|
||||||
.trim_end_matches(".lua")
|
.trim_end_matches(".lua")
|
||||||
.to_string();
|
.to_string();
|
||||||
let exit_code = lune.run(&script_name, &script).await?;
|
let (exit_code, _) = lune.run(&script_name, &script).await?;
|
||||||
Ok(exit_code)
|
Ok(ExitCode::from(exit_code))
|
||||||
}
|
}
|
||||||
)* }
|
)* }
|
||||||
}
|
}
|
||||||
|
@ -138,13 +140,16 @@ create_tests! {
|
||||||
process_cwd: "process/cwd",
|
process_cwd: "process/cwd",
|
||||||
process_env: "process/env",
|
process_env: "process/env",
|
||||||
process_exit: "process/exit",
|
process_exit: "process/exit",
|
||||||
process_spawn_async: "process/spawn/async",
|
process_exec_async: "process/exec/async",
|
||||||
process_spawn_basic: "process/spawn/basic",
|
process_exec_basic: "process/exec/basic",
|
||||||
process_spawn_cwd: "process/spawn/cwd",
|
process_exec_cwd: "process/exec/cwd",
|
||||||
process_spawn_no_panic: "process/spawn/no_panic",
|
process_exec_no_panic: "process/exec/no_panic",
|
||||||
process_spawn_shell: "process/spawn/shell",
|
process_exec_shell: "process/exec/shell",
|
||||||
process_spawn_stdin: "process/spawn/stdin",
|
process_exec_stdin: "process/exec/stdin",
|
||||||
process_spawn_stdio: "process/spawn/stdio",
|
process_exec_stdio: "process/exec/stdio",
|
||||||
|
process_spawn_non_blocking: "process/create/non_blocking",
|
||||||
|
process_spawn_status: "process/create/status",
|
||||||
|
process_spawn_stream: "process/create/stream",
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "std-regex")]
|
#[cfg(feature = "std-regex")]
|
||||||
|
@ -162,6 +167,7 @@ create_tests! {
|
||||||
roblox_datatype_color3: "roblox/datatypes/Color3",
|
roblox_datatype_color3: "roblox/datatypes/Color3",
|
||||||
roblox_datatype_color_sequence: "roblox/datatypes/ColorSequence",
|
roblox_datatype_color_sequence: "roblox/datatypes/ColorSequence",
|
||||||
roblox_datatype_color_sequence_keypoint: "roblox/datatypes/ColorSequenceKeypoint",
|
roblox_datatype_color_sequence_keypoint: "roblox/datatypes/ColorSequenceKeypoint",
|
||||||
|
roblox_datatype_content: "roblox/datatypes/Content",
|
||||||
roblox_datatype_enum: "roblox/datatypes/Enum",
|
roblox_datatype_enum: "roblox/datatypes/Enum",
|
||||||
roblox_datatype_faces: "roblox/datatypes/Faces",
|
roblox_datatype_faces: "roblox/datatypes/Faces",
|
||||||
roblox_datatype_font: "roblox/datatypes/Font",
|
roblox_datatype_font: "roblox/datatypes/Font",
|
||||||
|
|
|
@ -32,7 +32,7 @@ pub fn main() -> LuaResult<()> {
|
||||||
|
|
||||||
// Verify that we got a correct exit code
|
// Verify that we got a correct exit code
|
||||||
let code = sched.get_exit_code().unwrap_or_default();
|
let code = sched.get_exit_code().unwrap_or_default();
|
||||||
assert!(format!("{code:?}").contains("(1)"));
|
assert_eq!(code, 1);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use std::{cell::Cell, process::ExitCode, rc::Rc};
|
use std::{cell::Cell, rc::Rc};
|
||||||
|
|
||||||
use event_listener::Event;
|
use event_listener::Event;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct Exit {
|
pub(crate) struct Exit {
|
||||||
code: Rc<Cell<Option<ExitCode>>>,
|
code: Rc<Cell<Option<u8>>>,
|
||||||
event: Rc<Event>,
|
event: Rc<Event>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,12 +16,12 @@ impl Exit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(&self, code: ExitCode) {
|
pub fn set(&self, code: u8) {
|
||||||
self.code.set(Some(code));
|
self.code.set(Some(code));
|
||||||
self.event.notify(usize::MAX);
|
self.event.notify(usize::MAX);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self) -> Option<ExitCode> {
|
pub fn get(&self) -> Option<u8> {
|
||||||
self.code.get()
|
self.code.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,11 @@
|
||||||
#![allow(unused_imports)]
|
|
||||||
#![allow(clippy::too_many_lines)]
|
#![allow(clippy::too_many_lines)]
|
||||||
|
|
||||||
use std::process::ExitCode;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error_callback::ThreadErrorCallback,
|
error_callback::ThreadErrorCallback,
|
||||||
queue::{DeferredThreadQueue, SpawnedThreadQueue},
|
queue::{DeferredThreadQueue, SpawnedThreadQueue},
|
||||||
result_map::ThreadResultMap,
|
result_map::ThreadResultMap,
|
||||||
scheduler::Scheduler,
|
|
||||||
thread_id::ThreadId,
|
thread_id::ThreadId,
|
||||||
traits::LuaSchedulerExt,
|
traits::LuaSchedulerExt,
|
||||||
util::{is_poll_pending, LuaThreadOrFunction, ThreadResult},
|
util::{is_poll_pending, LuaThreadOrFunction, ThreadResult},
|
||||||
|
@ -232,7 +228,7 @@ impl<'lua> Functions<'lua> {
|
||||||
"exit",
|
"exit",
|
||||||
lua.create_function(|lua, code: Option<u8>| {
|
lua.create_function(|lua, code: Option<u8>| {
|
||||||
let _span = tracing::trace_span!("Scheduler::fn_exit").entered();
|
let _span = tracing::trace_span!("Scheduler::fn_exit").entered();
|
||||||
let code = code.map(ExitCode::from).unwrap_or_default();
|
let code = code.unwrap_or_default();
|
||||||
lua.set_exit_code(code);
|
lua.set_exit_code(code);
|
||||||
Ok(())
|
Ok(())
|
||||||
})?,
|
})?,
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
cell::Cell,
|
cell::Cell,
|
||||||
process::ExitCode,
|
|
||||||
rc::{Rc, Weak as WeakRc},
|
rc::{Rc, Weak as WeakRc},
|
||||||
sync::{Arc, Weak as WeakArc},
|
sync::{Arc, Weak as WeakArc},
|
||||||
thread::panicking,
|
thread::panicking,
|
||||||
|
@ -168,7 +167,7 @@ impl<'lua> Scheduler<'lua> {
|
||||||
Gets the exit code for this scheduler, if one has been set.
|
Gets the exit code for this scheduler, if one has been set.
|
||||||
*/
|
*/
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_exit_code(&self) -> Option<ExitCode> {
|
pub fn get_exit_code(&self) -> Option<u8> {
|
||||||
self.exit.get()
|
self.exit.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,7 +176,7 @@ impl<'lua> Scheduler<'lua> {
|
||||||
|
|
||||||
This will cause [`Scheduler::run`] to exit immediately.
|
This will cause [`Scheduler::run`] to exit immediately.
|
||||||
*/
|
*/
|
||||||
pub fn set_exit_code(&self, code: ExitCode) {
|
pub fn set_exit_code(&self, code: u8) {
|
||||||
self.exit.set(code);
|
self.exit.set(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,7 @@ pub trait LuaSchedulerExt<'lua> {
|
||||||
|
|
||||||
Panics if called outside of a running [`Scheduler`].
|
Panics if called outside of a running [`Scheduler`].
|
||||||
*/
|
*/
|
||||||
fn set_exit_code(&self, code: ExitCode);
|
fn set_exit_code(&self, code: u8);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Pushes (spawns) a lua thread to the **front** of the current scheduler.
|
Pushes (spawns) a lua thread to the **front** of the current scheduler.
|
||||||
|
@ -283,7 +283,7 @@ pub trait LuaSpawnExt<'lua> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'lua> LuaSchedulerExt<'lua> for Lua {
|
impl<'lua> LuaSchedulerExt<'lua> for Lua {
|
||||||
fn set_exit_code(&self, code: ExitCode) {
|
fn set_exit_code(&self, code: u8) {
|
||||||
let exit = self
|
let exit = self
|
||||||
.app_data_ref::<Exit>()
|
.app_data_ref::<Exit>()
|
||||||
.expect("exit code can only be set from within an active scheduler");
|
.expect("exit code can only be set from within an active scheduler");
|
||||||
|
@ -334,7 +334,7 @@ impl<'lua> LuaSchedulerExt<'lua> for Lua {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'lua> LuaSpawnExt<'lua> for Lua {
|
impl LuaSpawnExt<'_> for Lua {
|
||||||
fn spawn<F, T>(&self, fut: F) -> Task<T>
|
fn spawn<F, T>(&self, fut: F) -> Task<T>
|
||||||
where
|
where
|
||||||
F: Future<Output = T> + Send + 'static,
|
F: Future<Output = T> + Send + 'static,
|
||||||
|
|
4
rokit.toml
Normal file
4
rokit.toml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[tools]
|
||||||
|
luau-lsp = "JohnnyMorganz/luau-lsp@1.33.1"
|
||||||
|
stylua = "JohnnyMorganz/StyLua@0.20.0"
|
||||||
|
just = "casey/just@1.36.0"
|
|
@ -108,7 +108,7 @@ local BIN_ZLIB = if process.os == "macos" then "/opt/homebrew/bin/pigz" else "pi
|
||||||
|
|
||||||
local function checkInstalled(program: string, args: { string }?)
|
local function checkInstalled(program: string, args: { string }?)
|
||||||
print("Checking if", program, "is installed")
|
print("Checking if", program, "is installed")
|
||||||
local result = process.spawn(program, args)
|
local result = process.exec(program, args)
|
||||||
if not result.ok then
|
if not result.ok then
|
||||||
stdio.ewrite(string.format("Program '%s' is not installed\n", program))
|
stdio.ewrite(string.format("Program '%s' is not installed\n", program))
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
|
@ -123,7 +123,7 @@ checkInstalled(BIN_ZLIB, { "--version" })
|
||||||
-- Run them to generate files
|
-- Run them to generate files
|
||||||
|
|
||||||
local function run(program: string, args: { string }): string
|
local function run(program: string, args: { string }): string
|
||||||
local result = process.spawn(program, args)
|
local result = process.exec(program, args)
|
||||||
if not result.ok then
|
if not result.ok then
|
||||||
stdio.ewrite(string.format("Command '%s' failed\n", program))
|
stdio.ewrite(string.format("Command '%s' failed\n", program))
|
||||||
if #result.stdout > 0 then
|
if #result.stdout > 0 then
|
||||||
|
|
|
@ -31,7 +31,7 @@ if not runLocaleTests then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local dateCmd = process.spawn("bash", { "-c", "date +\"%A, %d %B %Y\" --date='@1693068988'" }, {
|
local dateCmd = process.exec("bash", { "-c", "date +\"%A, %d %B %Y\" --date='@1693068988'" }, {
|
||||||
env = {
|
env = {
|
||||||
LC_ALL = "fr_FR.UTF-8 ",
|
LC_ALL = "fr_FR.UTF-8 ",
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,10 +24,10 @@ local handle = net.serve(PORT, {
|
||||||
return "unreachable"
|
return "unreachable"
|
||||||
end,
|
end,
|
||||||
handleWebSocket = function(socket)
|
handleWebSocket = function(socket)
|
||||||
local socketMessage = socket.next()
|
local socketMessage = socket:next()
|
||||||
assert(socketMessage == REQUEST, "Invalid web socket request from client")
|
assert(socketMessage == REQUEST, "Invalid web socket request from client")
|
||||||
socket.send(RESPONSE)
|
socket:send(RESPONSE)
|
||||||
socket.close()
|
socket:close()
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -43,19 +43,19 @@ end)
|
||||||
|
|
||||||
local socket = net.socket(WS_URL)
|
local socket = net.socket(WS_URL)
|
||||||
|
|
||||||
socket.send(REQUEST)
|
socket:send(REQUEST)
|
||||||
|
|
||||||
local socketMessage = socket.next()
|
local socketMessage = socket:next()
|
||||||
assert(socketMessage ~= nil, "Got no web socket response from server")
|
assert(socketMessage ~= nil, "Got no web socket response from server")
|
||||||
assert(socketMessage == RESPONSE, "Invalid web socket response from server")
|
assert(socketMessage == RESPONSE, "Invalid web socket response from server")
|
||||||
|
|
||||||
socket.close()
|
socket:close()
|
||||||
|
|
||||||
task.cancel(thread2)
|
task.cancel(thread2)
|
||||||
|
|
||||||
-- Wait for the socket to close and make sure we can't send messages afterwards
|
-- Wait for the socket to close and make sure we can't send messages afterwards
|
||||||
task.wait()
|
task.wait()
|
||||||
local success3, err2 = (pcall :: any)(socket.send, "")
|
local success3, err2 = (pcall :: any)(socket.send, socket, "")
|
||||||
assert(not success3, "Sending messages after the socket has been closed should error")
|
assert(not success3, "Sending messages after the socket has been closed should error")
|
||||||
local message2 = tostring(err2)
|
local message2 = tostring(err2)
|
||||||
assert(
|
assert(
|
||||||
|
|
|
@ -8,17 +8,17 @@ assert(type(socket.send) == "function", "send must be a function")
|
||||||
assert(type(socket.close) == "function", "close must be a function")
|
assert(type(socket.close) == "function", "close must be a function")
|
||||||
|
|
||||||
-- Request to close the socket
|
-- Request to close the socket
|
||||||
socket.close()
|
socket:close()
|
||||||
|
|
||||||
-- Drain remaining messages, until we got our close message
|
-- Drain remaining messages, until we got our close message
|
||||||
while socket.next() do
|
while socket:next() do
|
||||||
end
|
end
|
||||||
|
|
||||||
assert(type(socket.closeCode) == "number", "closeCode should exist after closing")
|
assert(type(socket.closeCode) == "number", "closeCode should exist after closing")
|
||||||
assert(socket.closeCode == 1000, "closeCode should be 1000 after closing")
|
assert(socket.closeCode == 1000, "closeCode should be 1000 after closing")
|
||||||
|
|
||||||
local success, message = pcall(function()
|
local success, message = pcall(function()
|
||||||
socket.send("Hello, world!")
|
socket:send("Hello, world!")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert(not success, "send should fail after closing")
|
assert(not success, "send should fail after closing")
|
||||||
|
|
|
@ -8,7 +8,7 @@ local task = require("@lune/task")
|
||||||
local socket = net.socket("wss://gateway.discord.gg/?v=10&encoding=json")
|
local socket = net.socket("wss://gateway.discord.gg/?v=10&encoding=json")
|
||||||
|
|
||||||
while not socket.closeCode do
|
while not socket.closeCode do
|
||||||
local response = socket.next()
|
local response = socket:next()
|
||||||
|
|
||||||
if response then
|
if response then
|
||||||
local decodeSuccess, decodeMessage = pcall(serde.decode, "json" :: "json", response)
|
local decodeSuccess, decodeMessage = pcall(serde.decode, "json" :: "json", response)
|
||||||
|
@ -23,6 +23,6 @@ while not socket.closeCode do
|
||||||
|
|
||||||
-- Close the connection after a second with the success close code
|
-- Close the connection after a second with the success close code
|
||||||
task.wait(1)
|
task.wait(1)
|
||||||
socket.close(1000)
|
socket:close(1000)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@ local socket = net.socket("wss://gateway.discord.gg/?v=10&encoding=json")
|
||||||
|
|
||||||
local spawnedThread = task.spawn(function()
|
local spawnedThread = task.spawn(function()
|
||||||
while not socket.closeCode do
|
while not socket.closeCode do
|
||||||
socket.next()
|
socket:next()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
@ -23,9 +23,9 @@ end)
|
||||||
task.wait(1)
|
task.wait(1)
|
||||||
|
|
||||||
local payload = '{"op":1,"d":null}'
|
local payload = '{"op":1,"d":null}'
|
||||||
socket.send(payload)
|
socket:send(payload)
|
||||||
socket.send(buffer.fromstring(payload))
|
socket:send(buffer.fromstring(payload))
|
||||||
socket.close(1000)
|
socket:close(1000)
|
||||||
|
|
||||||
task.cancel(delayedThread)
|
task.cancel(delayedThread)
|
||||||
task.cancel(spawnedThread)
|
task.cancel(spawnedThread)
|
||||||
|
|
21
tests/process/create/kill.luau
Normal file
21
tests/process/create/kill.luau
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
-- Killing a child process should work as expected
|
||||||
|
|
||||||
|
local message = "Hello, world!"
|
||||||
|
local child = process.create("cat")
|
||||||
|
|
||||||
|
child.stdin:write(message)
|
||||||
|
child.kill()
|
||||||
|
|
||||||
|
assert(child.status().code == 9, "Child process should have an exit code of 9 (SIGKILL)")
|
||||||
|
|
||||||
|
assert(
|
||||||
|
child.stdout:readToEnd() == message,
|
||||||
|
"Reading from stdout of child process should work even after kill"
|
||||||
|
)
|
||||||
|
|
||||||
|
local stdinWriteOk = pcall(function()
|
||||||
|
child.stdin:write(message)
|
||||||
|
end)
|
||||||
|
assert(not stdinWriteOk, "Writing to stdin of child process should not work after kill")
|
13
tests/process/create/non_blocking.luau
Normal file
13
tests/process/create/non_blocking.luau
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
-- Spawning a child process should not block the thread
|
||||||
|
|
||||||
|
local childThread = coroutine.create(process.create)
|
||||||
|
|
||||||
|
local ok, err = coroutine.resume(childThread, "echo", { "hello, world" })
|
||||||
|
assert(ok, err)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
coroutine.status(childThread) == "dead",
|
||||||
|
"Child process should not block the thread it is running on"
|
||||||
|
)
|
15
tests/process/create/status.luau
Normal file
15
tests/process/create/status.luau
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
-- The exit code of an child process should be correct
|
||||||
|
|
||||||
|
local randomExitCode = math.random(0, 255)
|
||||||
|
local isOk = randomExitCode == 0
|
||||||
|
local child = process.create("exit", { tostring(randomExitCode) }, { shell = true })
|
||||||
|
local status = child.status()
|
||||||
|
|
||||||
|
assert(
|
||||||
|
status.code == randomExitCode,
|
||||||
|
`Child process exited with wrong exit code, expected {randomExitCode}`
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(status.ok == isOk, `Child status should be {if status.ok then "ok" else "not ok"}`)
|
18
tests/process/create/stream.luau
Normal file
18
tests/process/create/stream.luau
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
-- Should be able to write and read from child process streams
|
||||||
|
|
||||||
|
local msg = "hello, world"
|
||||||
|
|
||||||
|
local catChild = process.create("cat")
|
||||||
|
catChild.stdin:write(msg)
|
||||||
|
assert(
|
||||||
|
msg == catChild.stdout:read(#msg),
|
||||||
|
"Failed to write to stdin or read from stdout of child process"
|
||||||
|
)
|
||||||
|
|
||||||
|
local echoChild = if process.os == "windows"
|
||||||
|
then process.create("/c", { "echo", msg, "1>&2" }, { shell = "cmd" })
|
||||||
|
else process.create("echo", { msg, ">>/dev/stderr" }, { shell = true })
|
||||||
|
|
||||||
|
assert(msg == echoChild.stderr:read(#msg), "Failed to read from stderr of child process")
|
|
@ -4,7 +4,7 @@ local task = require("@lune/task")
|
||||||
|
|
||||||
local IS_WINDOWS = process.os == "windows"
|
local IS_WINDOWS = process.os == "windows"
|
||||||
|
|
||||||
-- Spawning a process should not block any lua thread(s)
|
-- Executing a command should not block any lua thread(s)
|
||||||
|
|
||||||
local SLEEP_DURATION = 1 / 4
|
local SLEEP_DURATION = 1 / 4
|
||||||
local SLEEP_SAMPLES = 2
|
local SLEEP_SAMPLES = 2
|
||||||
|
@ -31,7 +31,7 @@ for i = 1, SLEEP_SAMPLES, 1 do
|
||||||
table.insert(args, 1, "-Milliseconds")
|
table.insert(args, 1, "-Milliseconds")
|
||||||
end
|
end
|
||||||
-- Windows does not have `sleep` as a process, so we use powershell instead.
|
-- Windows does not have `sleep` as a process, so we use powershell instead.
|
||||||
process.spawn("sleep", args, if IS_WINDOWS then { shell = true } else nil)
|
process.exec("sleep", args, if IS_WINDOWS then { shell = true } else nil)
|
||||||
sleepCounter += 1
|
sleepCounter += 1
|
||||||
end)
|
end)
|
||||||
end
|
end
|
|
@ -2,7 +2,7 @@ local process = require("@lune/process")
|
||||||
local stdio = require("@lune/stdio")
|
local stdio = require("@lune/stdio")
|
||||||
local task = require("@lune/task")
|
local task = require("@lune/task")
|
||||||
|
|
||||||
-- Spawning a child process should work, with options
|
-- Executing a command should work, with options
|
||||||
|
|
||||||
local thread = task.delay(1, function()
|
local thread = task.delay(1, function()
|
||||||
stdio.ewrite("Spawning a process should take a reasonable amount of time\n")
|
stdio.ewrite("Spawning a process should take a reasonable amount of time\n")
|
||||||
|
@ -12,7 +12,7 @@ end)
|
||||||
|
|
||||||
local IS_WINDOWS = process.os == "windows"
|
local IS_WINDOWS = process.os == "windows"
|
||||||
|
|
||||||
local result = process.spawn(
|
local result = process.exec(
|
||||||
if IS_WINDOWS then "cmd" else "ls",
|
if IS_WINDOWS then "cmd" else "ls",
|
||||||
if IS_WINDOWS then { "/c", "dir" } else { "-a" }
|
if IS_WINDOWS then { "/c", "dir" } else { "-a" }
|
||||||
)
|
)
|
|
@ -6,7 +6,7 @@ local pwdCommand = if IS_WINDOWS then "cmd" else "pwd"
|
||||||
local pwdArgs = if IS_WINDOWS then { "/c", "cd" } else {}
|
local pwdArgs = if IS_WINDOWS then { "/c", "cd" } else {}
|
||||||
|
|
||||||
-- Make sure the cwd option actually uses the directory we want
|
-- Make sure the cwd option actually uses the directory we want
|
||||||
local rootPwd = process.spawn(pwdCommand, pwdArgs, {
|
local rootPwd = process.exec(pwdCommand, pwdArgs, {
|
||||||
cwd = "/",
|
cwd = "/",
|
||||||
}).stdout
|
}).stdout
|
||||||
rootPwd = string.gsub(rootPwd, "^%s+", "")
|
rootPwd = string.gsub(rootPwd, "^%s+", "")
|
||||||
|
@ -27,24 +27,24 @@ end
|
||||||
|
|
||||||
-- Setting cwd should not change the cwd of this process
|
-- Setting cwd should not change the cwd of this process
|
||||||
|
|
||||||
local pwdBefore = process.spawn(pwdCommand, pwdArgs).stdout
|
local pwdBefore = process.exec(pwdCommand, pwdArgs).stdout
|
||||||
process.spawn("ls", {}, {
|
process.exec("ls", {}, {
|
||||||
cwd = "/",
|
cwd = "/",
|
||||||
shell = true,
|
shell = true,
|
||||||
})
|
})
|
||||||
local pwdAfter = process.spawn(pwdCommand, pwdArgs).stdout
|
local pwdAfter = process.exec(pwdCommand, pwdArgs).stdout
|
||||||
assert(pwdBefore == pwdAfter, "Current working directory changed after running child process")
|
assert(pwdBefore == pwdAfter, "Current working directory changed after running child process")
|
||||||
|
|
||||||
-- Setting the cwd on a child process should properly
|
-- Setting the cwd on a child process should properly
|
||||||
-- replace any leading ~ with the users real home dir
|
-- replace any leading ~ with the users real home dir
|
||||||
|
|
||||||
local homeDir1 = process.spawn("echo $HOME", nil, {
|
local homeDir1 = process.exec("echo $HOME", nil, {
|
||||||
shell = true,
|
shell = true,
|
||||||
}).stdout
|
}).stdout
|
||||||
|
|
||||||
-- NOTE: Powershell for windows uses `$pwd.Path` instead of `pwd` as pwd would return
|
-- NOTE: Powershell for windows uses `$pwd.Path` instead of `pwd` as pwd would return
|
||||||
-- a PathInfo object, using $pwd.Path gets the Path property of the PathInfo object
|
-- a PathInfo object, using $pwd.Path gets the Path property of the PathInfo object
|
||||||
local homeDir2 = process.spawn(if IS_WINDOWS then "$pwd.Path" else "pwd", nil, {
|
local homeDir2 = process.exec(if IS_WINDOWS then "$pwd.Path" else "pwd", nil, {
|
||||||
shell = true,
|
shell = true,
|
||||||
cwd = "~",
|
cwd = "~",
|
||||||
}).stdout
|
}).stdout
|
7
tests/process/exec/no_panic.luau
Normal file
7
tests/process/exec/no_panic.luau
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
-- Executing a non existent command as a child process
|
||||||
|
-- should not panic, but should error
|
||||||
|
|
||||||
|
local success = pcall(process.exec, "someProgramThatDoesNotExist")
|
||||||
|
assert(not success, "Spawned a non-existent program")
|
|
@ -5,7 +5,7 @@ local IS_WINDOWS = process.os == "windows"
|
||||||
-- Default shell should be /bin/sh on unix and powershell on Windows,
|
-- Default shell should be /bin/sh on unix and powershell on Windows,
|
||||||
-- note that powershell needs slightly different command flags for ls
|
-- note that powershell needs slightly different command flags for ls
|
||||||
|
|
||||||
local shellResult = process.spawn("ls", {
|
local shellResult = process.exec("ls", {
|
||||||
if IS_WINDOWS then "-Force" else "-a",
|
if IS_WINDOWS then "-Force" else "-a",
|
||||||
}, {
|
}, {
|
||||||
shell = true,
|
shell = true,
|
|
@ -10,8 +10,8 @@ local echoMessage = "Hello from child process!"
|
||||||
-- When passing stdin to powershell on windows we must "accept" using the double newline
|
-- When passing stdin to powershell on windows we must "accept" using the double newline
|
||||||
|
|
||||||
local result = if IS_WINDOWS
|
local result = if IS_WINDOWS
|
||||||
then process.spawn("powershell", { "echo" }, { stdin = echoMessage .. "\n\n" })
|
then process.exec("powershell", { "echo" }, { stdin = echoMessage .. "\n\n" })
|
||||||
else process.spawn("xargs", { "echo" }, { stdin = echoMessage })
|
else process.exec("xargs", { "echo" }, { stdin = echoMessage })
|
||||||
|
|
||||||
local resultStdout = if IS_WINDOWS
|
local resultStdout = if IS_WINDOWS
|
||||||
then string.sub(result.stdout, #result.stdout - #echoMessage - 1)
|
then string.sub(result.stdout, #result.stdout - #echoMessage - 1)
|
|
@ -5,12 +5,12 @@ local IS_WINDOWS = process.os == "windows"
|
||||||
-- Inheriting stdio & environment variables should work
|
-- Inheriting stdio & environment variables should work
|
||||||
|
|
||||||
local echoMessage = "Hello from child process!"
|
local echoMessage = "Hello from child process!"
|
||||||
local echoResult = process.spawn("echo", {
|
local echoResult = process.exec("echo", {
|
||||||
if IS_WINDOWS then '"$Env:TEST_VAR"' else '"$TEST_VAR"',
|
if IS_WINDOWS then '"$Env:TEST_VAR"' else '"$TEST_VAR"',
|
||||||
}, {
|
}, {
|
||||||
env = { TEST_VAR = echoMessage },
|
env = { TEST_VAR = echoMessage },
|
||||||
shell = if IS_WINDOWS then "powershell" else "bash",
|
shell = if IS_WINDOWS then "powershell" else "bash",
|
||||||
stdio = "inherit",
|
stdio = "inherit" :: process.SpawnOptionsStdioKind, -- FIXME: This should just work without a cast?
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Windows uses \r\n (CRLF) and unix uses \n (LF)
|
-- Windows uses \r\n (CRLF) and unix uses \n (LF)
|
|
@ -1,7 +0,0 @@
|
||||||
local process = require("@lune/process")
|
|
||||||
|
|
||||||
-- Spawning a child process for a non-existent
|
|
||||||
-- program should not panic, but should error
|
|
||||||
|
|
||||||
local success = pcall(process.spawn, "someProgramThatDoesNotExist")
|
|
||||||
assert(not success, "Spawned a non-existent program")
|
|
62
tests/roblox/datatypes/Content.luau
Normal file
62
tests/roblox/datatypes/Content.luau
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
local roblox = require("@lune/roblox") :: any
|
||||||
|
local Content = roblox.Content
|
||||||
|
local Instance = roblox.Instance
|
||||||
|
local Enum = roblox.Enum
|
||||||
|
|
||||||
|
assert(Content.none, "Content.none did not exist")
|
||||||
|
assert(
|
||||||
|
Content.none.SourceType == Enum.ContentSourceType.None,
|
||||||
|
"Content.none's SourceType was wrong"
|
||||||
|
)
|
||||||
|
assert(Content.none.Uri == nil, "Content.none's Uri field was wrong")
|
||||||
|
assert(Content.none.Object == nil, "Content.none's Object field was wrong")
|
||||||
|
|
||||||
|
local uri = Content.fromUri("test uri")
|
||||||
|
assert(uri.SourceType == Enum.ContentSourceType.Uri, "URI Content's SourceType was wrong")
|
||||||
|
assert(uri.Uri == "test uri", "URI Content's Uri field was wrong")
|
||||||
|
assert(uri.Object == nil, "URI Content's Object field was wrong")
|
||||||
|
|
||||||
|
assert(not pcall(Content.fromUri), "Content.fromUri accepted no argument")
|
||||||
|
assert(not pcall(Content.fromUri, false), "Content.fromUri accepted a boolean argument")
|
||||||
|
assert(not pcall(Content.fromUri, Enum), "Content.fromUri accepted a UserData as an argument")
|
||||||
|
assert(
|
||||||
|
not pcall(Content.fromUri, buffer.create(0)),
|
||||||
|
"Content.fromUri accepted a buffer as an argument"
|
||||||
|
)
|
||||||
|
|
||||||
|
-- It feels weird that this is allowed because `EditableImage` is very much
|
||||||
|
-- not an Instance. But what can you do?
|
||||||
|
local target = Instance.new("EditableImage")
|
||||||
|
local object = Content.fromObject(target)
|
||||||
|
assert(object.SourceType == Enum.ContentSourceType.Object, "Object Content's SourceType was wrong")
|
||||||
|
assert(object.Uri == nil, "Object Content's Uri field was wrong")
|
||||||
|
assert(object.Object == target, "Object Content's Object field was wrong")
|
||||||
|
|
||||||
|
assert(not pcall(Content.fromObject), "Content.fromObject accepted no argument")
|
||||||
|
assert(not pcall(Content.fromObject, false), "Content.fromObject accepted a boolean argument")
|
||||||
|
assert(
|
||||||
|
not pcall(Content.fromObject, Enum),
|
||||||
|
"Content.fromObject accepted a non-Instance/Object UserData as an argument"
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
not pcall(Content.fromObject, buffer.create(0)),
|
||||||
|
"Content.fromObject accepted a buffer as an argument"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
not pcall(Content.fromObject, Instance.new("Folder")),
|
||||||
|
"Content.fromObject accepted an Instance as an argument"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
tostring(Content.none) == "Content(None)",
|
||||||
|
`expected tostring(Content.none) to be Content(None), it was actually {Content.none}`
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
tostring(uri) == "Content(Uri=test uri)",
|
||||||
|
`expected tostring(URI Content) to be Content(Uri=...), it was actually {uri}`
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
tostring(object) == "Content(Object)",
|
||||||
|
`expected tostring(Object Content) to be Content(Object), it was actually {object}`
|
||||||
|
)
|
|
@ -16,6 +16,7 @@ local UDim2 = roblox.UDim2
|
||||||
local Vector2 = roblox.Vector2
|
local Vector2 = roblox.Vector2
|
||||||
local Vector3 = roblox.Vector3
|
local Vector3 = roblox.Vector3
|
||||||
local Instance = roblox.Instance
|
local Instance = roblox.Instance
|
||||||
|
local Enum = roblox.Enum
|
||||||
|
|
||||||
local modelFile = fs.readFile("tests/roblox/rbx-test-files/models/attributes/binary.rbxm")
|
local modelFile = fs.readFile("tests/roblox/rbx-test-files/models/attributes/binary.rbxm")
|
||||||
local model = roblox.deserializeModel(modelFile)[1]
|
local model = roblox.deserializeModel(modelFile)[1]
|
||||||
|
@ -114,3 +115,12 @@ model.Parent = game
|
||||||
local placeFile = roblox.serializePlace(game)
|
local placeFile = roblox.serializePlace(game)
|
||||||
fs.writeDir("bin/roblox")
|
fs.writeDir("bin/roblox")
|
||||||
fs.writeFile("bin/roblox/attributes.rbxl", placeFile)
|
fs.writeFile("bin/roblox/attributes.rbxl", placeFile)
|
||||||
|
|
||||||
|
local enum_attr = Instance.new("Folder")
|
||||||
|
enum_attr:SetAttribute("Foo", Enum.NormalId.Front)
|
||||||
|
assert(enum_attr:GetAttribute("Foo") == Enum.NormalId.Front)
|
||||||
|
|
||||||
|
local enum_attr_ser = roblox.serializeModel({ enum_attr })
|
||||||
|
local enum_attr_de = roblox.deserializeModel(enum_attr_ser)
|
||||||
|
|
||||||
|
assert(enum_attr_de[1]:GetAttribute("Foo") == Enum.NormalId.Front)
|
||||||
|
|
|
@ -20,14 +20,10 @@ assert(not pcall(function()
|
||||||
return child1.Name
|
return child1.Name
|
||||||
end))
|
end))
|
||||||
|
|
||||||
assert(not pcall(function()
|
assert(not child1.Parent)
|
||||||
return child1.Parent
|
|
||||||
end))
|
|
||||||
|
|
||||||
assert(not pcall(function()
|
assert(not pcall(function()
|
||||||
return child2.Name
|
return child2.Name
|
||||||
end))
|
end))
|
||||||
|
|
||||||
assert(not pcall(function()
|
assert(not child2.Parent)
|
||||||
return child2.Parent
|
|
||||||
end))
|
|
||||||
|
|
|
@ -14,22 +14,16 @@ assert(not pcall(function()
|
||||||
return root.Name
|
return root.Name
|
||||||
end))
|
end))
|
||||||
|
|
||||||
assert(not pcall(function()
|
assert(not root.Parent)
|
||||||
return root.Parent
|
|
||||||
end))
|
|
||||||
|
|
||||||
assert(not pcall(function()
|
assert(not pcall(function()
|
||||||
return child.Name
|
return child.Name
|
||||||
end))
|
end))
|
||||||
|
|
||||||
assert(not pcall(function()
|
assert(not child.Parent)
|
||||||
return child.Parent
|
|
||||||
end))
|
|
||||||
|
|
||||||
assert(not pcall(function()
|
assert(not pcall(function()
|
||||||
return descendant.Name
|
return descendant.Name
|
||||||
end))
|
end))
|
||||||
|
|
||||||
assert(not pcall(function()
|
assert(not descendant.Parent)
|
||||||
return descendant.Parent
|
|
||||||
end))
|
|
||||||
|
|
|
@ -33,6 +33,16 @@ part.Shape = Enum.PartType.Ball
|
||||||
|
|
||||||
assert(part.Shape == Enum.PartType.Ball)
|
assert(part.Shape == Enum.PartType.Ball)
|
||||||
|
|
||||||
|
-- Enums should roundtrip through serde without problem
|
||||||
|
|
||||||
|
local decal = Instance.new("Decal")
|
||||||
|
decal.Face = Enum.NormalId.Top
|
||||||
|
|
||||||
|
local decal_ser = roblox.serializeModel({ decal })
|
||||||
|
local decal_de = roblox.deserializeModel(decal_ser)
|
||||||
|
|
||||||
|
assert(decal_de[1].Face == Enum.NormalId.Top)
|
||||||
|
|
||||||
-- Properties that don't exist for a class should error
|
-- Properties that don't exist for a class should error
|
||||||
|
|
||||||
local meshPart = Instance.new("MeshPart")
|
local meshPart = Instance.new("MeshPart")
|
||||||
|
|
|
@ -109,7 +109,7 @@ assertContains(
|
||||||
|
|
||||||
local _, errorMessage = pcall(function()
|
local _, errorMessage = pcall(function()
|
||||||
local function innerInnerFn()
|
local function innerInnerFn()
|
||||||
process.spawn("PROGRAM_THAT_DOES_NOT_EXIST")
|
process.exec("PROGRAM_THAT_DOES_NOT_EXIST")
|
||||||
end
|
end
|
||||||
local function innerFn()
|
local function innerFn()
|
||||||
innerInnerFn()
|
innerInnerFn()
|
||||||
|
@ -122,3 +122,23 @@ stdio.ewrite(typeof(errorMessage))
|
||||||
assertContains("Should format errors similarly to userdata", stdio.format(errorMessage), "<LuaErr")
|
assertContains("Should format errors similarly to userdata", stdio.format(errorMessage), "<LuaErr")
|
||||||
assertContains("Should format errors with stack begins", stdio.format(errorMessage), "Stack Begin")
|
assertContains("Should format errors with stack begins", stdio.format(errorMessage), "Stack Begin")
|
||||||
assertContains("Should format errors with stack ends", stdio.format(errorMessage), "Stack End")
|
assertContains("Should format errors with stack ends", stdio.format(errorMessage), "Stack End")
|
||||||
|
|
||||||
|
-- Check that calling stdio.format in a __tostring metamethod by print doesn't cause a deadlock
|
||||||
|
|
||||||
|
local inner = {}
|
||||||
|
setmetatable(inner, {
|
||||||
|
__tostring = function()
|
||||||
|
return stdio.format(5)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
print(inner)
|
||||||
|
|
||||||
|
local outer = {}
|
||||||
|
setmetatable(outer, {
|
||||||
|
__tostring = function()
|
||||||
|
return stdio.format(inner)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
print(outer)
|
||||||
|
|
|
@ -87,10 +87,19 @@ export type DateTimeValueArguments = DateTimeValues & OptionalMillisecond
|
||||||
]=]
|
]=]
|
||||||
export type DateTimeValueReturns = DateTimeValues & Millisecond
|
export type DateTimeValueReturns = DateTimeValues & Millisecond
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@prop unixTimestamp number
|
||||||
|
@within DateTime
|
||||||
|
Number of seconds passed since the UNIX epoch.
|
||||||
|
]=]
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@prop unixTimestampMillis number
|
||||||
|
@within DateTime
|
||||||
|
Number of milliseconds passed since the UNIX epoch.
|
||||||
|
]=]
|
||||||
local DateTime = {
|
local DateTime = {
|
||||||
--- Number of seconds passed since the UNIX epoch.
|
|
||||||
unixTimestamp = (nil :: any) :: number,
|
unixTimestamp = (nil :: any) :: number,
|
||||||
--- Number of milliseconds passed since the UNIX epoch.
|
|
||||||
unixTimestampMillis = (nil :: any) :: number,
|
unixTimestampMillis = (nil :: any) :: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -173,9 +173,9 @@ export type ServeHandle = {
|
||||||
]=]
|
]=]
|
||||||
export type WebSocket = {
|
export type WebSocket = {
|
||||||
closeCode: number?,
|
closeCode: number?,
|
||||||
close: (code: number?) -> (),
|
close: (self: WebSocket, code: number?) -> (),
|
||||||
send: (message: (string | buffer)?, asBinaryMessage: boolean?) -> (),
|
send: (self: WebSocket, message: (string | buffer)?, asBinaryMessage: boolean?) -> (),
|
||||||
next: () -> string?,
|
next: (self: WebSocket) -> string?,
|
||||||
}
|
}
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
export type OS = "linux" | "macos" | "windows"
|
export type OS = "linux" | "macos" | "windows"
|
||||||
export type Arch = "x86_64" | "aarch64"
|
export type Arch = "x86_64" | "aarch64"
|
||||||
|
export type Endianness = "big" | "little"
|
||||||
|
|
||||||
export type SpawnOptionsStdioKind = "default" | "inherit" | "forward" | "none"
|
export type SpawnOptionsStdioKind = "default" | "inherit" | "forward" | "none"
|
||||||
export type SpawnOptionsStdio = {
|
export type SpawnOptionsStdio = {
|
||||||
stdout: SpawnOptionsStdioKind?,
|
stdout: SpawnOptionsStdioKind?,
|
||||||
stderr: SpawnOptionsStdioKind?,
|
stderr: SpawnOptionsStdioKind?,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExecuteOptionsStdio = SpawnOptionsStdio & {
|
||||||
stdin: string?,
|
stdin: string?,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,27 +16,117 @@ export type SpawnOptionsStdio = {
|
||||||
@interface SpawnOptions
|
@interface SpawnOptions
|
||||||
@within Process
|
@within Process
|
||||||
|
|
||||||
A dictionary of options for `process.spawn`, with the following available values:
|
A dictionary of options for `process.create`, with the following available values:
|
||||||
|
|
||||||
* `cwd` - The current working directory for the process
|
* `cwd` - The current working directory for the process
|
||||||
* `env` - Extra environment variables to give to the process
|
* `env` - Extra environment variables to give to the process
|
||||||
* `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
|
* `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
|
||||||
* `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `SpawnOptionsStdio` for more info
|
* `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `SpawnOptionsStdio` for more info
|
||||||
* `stdin` - Optional standard input to pass to spawned child process
|
|
||||||
]=]
|
]=]
|
||||||
export type SpawnOptions = {
|
export type SpawnOptions = {
|
||||||
cwd: string?,
|
cwd: string?,
|
||||||
env: { [string]: string }?,
|
env: { [string]: string }?,
|
||||||
shell: (boolean | string)?,
|
shell: (boolean | string)?,
|
||||||
|
}
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@interface ExecuteOptions
|
||||||
|
@within Process
|
||||||
|
|
||||||
|
A dictionary of options for `process.exec`, with the following available values:
|
||||||
|
|
||||||
|
* `cwd` - The current working directory for the process
|
||||||
|
* `env` - Extra environment variables to give to the process
|
||||||
|
* `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
|
||||||
|
* `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `ExecuteOptionsStdio` for more info
|
||||||
|
* `stdin` - Optional standard input to pass to executed child process
|
||||||
|
]=]
|
||||||
|
export type ExecuteOptions = SpawnOptions & {
|
||||||
stdio: (SpawnOptionsStdioKind | SpawnOptionsStdio)?,
|
stdio: (SpawnOptionsStdioKind | SpawnOptionsStdio)?,
|
||||||
stdin: string?, -- TODO: Remove this since it is now available in stdio above, breaking change
|
stdin: string?, -- TODO: Remove this since it is now available in stdio above, breaking change
|
||||||
}
|
}
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
@interface SpawnResult
|
@class ChildProcessReader
|
||||||
@within Process
|
@within Process
|
||||||
|
|
||||||
Result type for child processes in `process.spawn`.
|
A reader class to read data from a child process' streams in realtime.
|
||||||
|
]=]
|
||||||
|
local ChildProcessReader = {}
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@within ChildProcessReader
|
||||||
|
|
||||||
|
Reads a chunk of data (specified length or a default of 8 bytes at a time) from
|
||||||
|
the reader as a string. Returns nil if there is no more data to read.
|
||||||
|
|
||||||
|
This function may yield until there is new data to read from reader, if all data
|
||||||
|
till present has already been read, and the process has not exited.
|
||||||
|
|
||||||
|
@return The string containing the data read from the reader
|
||||||
|
]=]
|
||||||
|
function ChildProcessReader:read(chunkSize: number?): string?
|
||||||
|
return nil :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@within ChildProcessReader
|
||||||
|
|
||||||
|
Reads all the data currently present in the reader as a string.
|
||||||
|
This function will yield until the process exits.
|
||||||
|
|
||||||
|
@return The string containing the data read from the reader
|
||||||
|
]=]
|
||||||
|
function ChildProcessReader:readToEnd(): string
|
||||||
|
return nil :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@class ChildProcessWriter
|
||||||
|
@within Process
|
||||||
|
|
||||||
|
A writer class to write data to a child process' streams in realtime.
|
||||||
|
]=]
|
||||||
|
local ChildProcessWriter = {}
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@within ChildProcessWriter
|
||||||
|
|
||||||
|
Writes a buffer or string of data to the writer.
|
||||||
|
|
||||||
|
@param data The data to write to the writer
|
||||||
|
]=]
|
||||||
|
function ChildProcessWriter:write(data: buffer | string): ()
|
||||||
|
return nil :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@interface ChildProcess
|
||||||
|
@within Process
|
||||||
|
|
||||||
|
Result type for child processes in `process.create`.
|
||||||
|
|
||||||
|
This is a dictionary containing the following values:
|
||||||
|
|
||||||
|
* `stdin` - A writer to write to the child process' stdin - see `ChildProcessWriter` for more info
|
||||||
|
* `stdout` - A reader to read from the child process' stdout - see `ChildProcessReader` for more info
|
||||||
|
* `stderr` - A reader to read from the child process' stderr - see `ChildProcessReader` for more info
|
||||||
|
* `kill` - A function that kills the child process
|
||||||
|
* `status` - A function that yields and returns the exit status of the child process
|
||||||
|
]=]
|
||||||
|
export type ChildProcess = {
|
||||||
|
stdin: typeof(ChildProcessWriter),
|
||||||
|
stdout: typeof(ChildProcessReader),
|
||||||
|
stderr: typeof(ChildProcessReader),
|
||||||
|
kill: () -> (),
|
||||||
|
status: () -> { ok: boolean, code: number },
|
||||||
|
}
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@interface ExecuteResult
|
||||||
|
@within Process
|
||||||
|
|
||||||
|
Result type for child processes in `process.exec`.
|
||||||
|
|
||||||
This is a dictionary containing the following values:
|
This is a dictionary containing the following values:
|
||||||
|
|
||||||
|
@ -41,7 +135,7 @@ export type SpawnOptions = {
|
||||||
* `stdout` - The full contents written to stdout by the child process, or an empty string if nothing was written
|
* `stdout` - The full contents written to stdout by the child process, or an empty string if nothing was written
|
||||||
* `stderr` - The full contents written to stderr by the child process, or an empty string if nothing was written
|
* `stderr` - The full contents written to stderr by the child process, or an empty string if nothing was written
|
||||||
]=]
|
]=]
|
||||||
export type SpawnResult = {
|
export type ExecuteResult = {
|
||||||
ok: boolean,
|
ok: boolean,
|
||||||
code: number,
|
code: number,
|
||||||
stdout: string,
|
stdout: string,
|
||||||
|
@ -73,8 +167,8 @@ export type SpawnResult = {
|
||||||
-- Getting the current os and processor architecture
|
-- Getting the current os and processor architecture
|
||||||
print("Running " .. process.os .. " on " .. process.arch .. "!")
|
print("Running " .. process.os .. " on " .. process.arch .. "!")
|
||||||
|
|
||||||
-- Spawning a child process
|
-- Executing a command
|
||||||
local result = process.spawn("program", {
|
local result = process.exec("program", {
|
||||||
"cli argument",
|
"cli argument",
|
||||||
"other cli argument"
|
"other cli argument"
|
||||||
})
|
})
|
||||||
|
@ -83,6 +177,19 @@ export type SpawnResult = {
|
||||||
else
|
else
|
||||||
print(result.stderr)
|
print(result.stderr)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Spawning a child process
|
||||||
|
local child = process.create("program", {
|
||||||
|
"cli argument",
|
||||||
|
"other cli argument"
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Writing to the child process' stdin
|
||||||
|
child.stdin:write("Hello from Lune!")
|
||||||
|
|
||||||
|
-- Reading from the child process' stdout
|
||||||
|
local data = child.stdout:read()
|
||||||
|
print(buffer.tostring(data))
|
||||||
```
|
```
|
||||||
]=]
|
]=]
|
||||||
local process = {}
|
local process = {}
|
||||||
|
@ -116,6 +223,20 @@ process.os = (nil :: any) :: OS
|
||||||
]=]
|
]=]
|
||||||
process.arch = (nil :: any) :: Arch
|
process.arch = (nil :: any) :: Arch
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@within Process
|
||||||
|
@prop endianness Endianness
|
||||||
|
@tag read_only
|
||||||
|
|
||||||
|
The endianness of the processor currently being used.
|
||||||
|
|
||||||
|
Possible values:
|
||||||
|
|
||||||
|
* `"big"`
|
||||||
|
* `"little"`
|
||||||
|
]=]
|
||||||
|
process.endianness = (nil :: any) :: Endianness
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
@within Process
|
@within Process
|
||||||
@prop args { string }
|
@prop args { string }
|
||||||
|
@ -163,19 +284,44 @@ end
|
||||||
--[=[
|
--[=[
|
||||||
@within Process
|
@within Process
|
||||||
|
|
||||||
Spawns a child process that will run the program `program`, and returns a dictionary that describes the final status and ouput of the child process.
|
Spawns a child process in the background that runs the program `program`, and immediately returns
|
||||||
|
readers and writers to communicate with it.
|
||||||
|
|
||||||
|
In order to execute a command and wait for its output, see `process.exec`.
|
||||||
|
|
||||||
The second argument, `params`, can be passed as a list of string parameters to give to the program.
|
The second argument, `params`, can be passed as a list of string parameters to give to the program.
|
||||||
|
|
||||||
The third argument, `options`, can be passed as a dictionary of options to give to the child process.
|
The third argument, `options`, can be passed as a dictionary of options to give to the child process.
|
||||||
Refer to the documentation for `SpawnOptions` for specific option keys and their values.
|
Refer to the documentation for `SpawnOptions` for specific option keys and their values.
|
||||||
|
|
||||||
@param program The program to spawn as a child process
|
@param program The program to Execute as a child process
|
||||||
|
@param params Additional parameters to pass to the program
|
||||||
|
@param options A dictionary of options for the child process
|
||||||
|
@return A dictionary with the readers and writers to communicate with the child process
|
||||||
|
]=]
|
||||||
|
function process.create(program: string, params: { string }?, options: SpawnOptions?): ChildProcess
|
||||||
|
return nil :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@within Process
|
||||||
|
|
||||||
|
Executes a child process that will execute the command `program`, waiting for it to exit.
|
||||||
|
Upon exit, it returns a dictionary that describes the final status and ouput of the child process.
|
||||||
|
|
||||||
|
In order to spawn a child process in the background, see `process.create`.
|
||||||
|
|
||||||
|
The second argument, `params`, can be passed as a list of string parameters to give to the program.
|
||||||
|
|
||||||
|
The third argument, `options`, can be passed as a dictionary of options to give to the child process.
|
||||||
|
Refer to the documentation for `ExecuteOptions` for specific option keys and their values.
|
||||||
|
|
||||||
|
@param program The program to Execute as a child process
|
||||||
@param params Additional parameters to pass to the program
|
@param params Additional parameters to pass to the program
|
||||||
@param options A dictionary of options for the child process
|
@param options A dictionary of options for the child process
|
||||||
@return A dictionary representing the result of the child process
|
@return A dictionary representing the result of the child process
|
||||||
]=]
|
]=]
|
||||||
function process.spawn(program: string, params: { string }?, options: SpawnOptions?): SpawnResult
|
function process.exec(program: string, params: { string }?, options: ExecuteOptions?): ExecuteResult
|
||||||
return nil :: any
|
return nil :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -19,67 +19,82 @@ local RegexMatch = {
|
||||||
|
|
||||||
type RegexMatch = typeof(RegexMatch)
|
type RegexMatch = typeof(RegexMatch)
|
||||||
|
|
||||||
|
local RegexCaptures = {}
|
||||||
|
|
||||||
|
function RegexCaptures.get(self: RegexCaptures, index: number): RegexMatch?
|
||||||
|
return nil :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
function RegexCaptures.group(self: RegexCaptures, group: string): RegexMatch?
|
||||||
|
return nil :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
function RegexCaptures.format(self: RegexCaptures, format: string): string
|
||||||
|
return nil :: any
|
||||||
|
end
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
@class RegexCaptures
|
@class RegexCaptures
|
||||||
|
|
||||||
Captures from a regular expression.
|
Captures from a regular expression.
|
||||||
]=]
|
]=]
|
||||||
local RegexCaptures = {}
|
export type RegexCaptures = typeof(setmetatable(
|
||||||
|
{} :: {
|
||||||
|
--[=[
|
||||||
|
@within RegexCaptures
|
||||||
|
@tag Method
|
||||||
|
@method get
|
||||||
|
|
||||||
--[=[
|
Returns the match at the given index, if one exists.
|
||||||
@within RegexCaptures
|
|
||||||
@tag Method
|
|
||||||
|
|
||||||
Returns the match at the given index, if one exists.
|
@param index -- The index of the match to get
|
||||||
|
@return RegexMatch -- The match, if one exists
|
||||||
|
]=]
|
||||||
|
|
||||||
@param index -- The index of the match to get
|
get: (self: RegexCaptures, index: number) -> RegexMatch?,
|
||||||
@return RegexMatch -- The match, if one exists
|
|
||||||
]=]
|
|
||||||
function RegexCaptures.get(self: RegexCaptures, index: number): RegexMatch?
|
|
||||||
return nil :: any
|
|
||||||
end
|
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
@within RegexCaptures
|
@within RegexCaptures
|
||||||
@tag Method
|
@tag Method
|
||||||
|
@method group
|
||||||
|
|
||||||
Returns the match for the given named match group, if one exists.
|
Returns the match for the given named match group, if one exists.
|
||||||
|
|
||||||
@param group -- The name of the group to get
|
@param group -- The name of the group to get
|
||||||
@return RegexMatch -- The match, if one exists
|
@return RegexMatch -- The match, if one exists
|
||||||
]=]
|
]=]
|
||||||
function RegexCaptures.group(self: RegexCaptures, group: string): RegexMatch?
|
group: (self: RegexCaptures, group: string) -> RegexMatch?,
|
||||||
return nil :: any
|
|
||||||
end
|
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
@within RegexCaptures
|
@within RegexCaptures
|
||||||
@tag Method
|
@tag Method
|
||||||
|
@method format
|
||||||
|
|
||||||
Formats the captures using the given format string.
|
Formats the captures using the given format string.
|
||||||
|
|
||||||
### Example usage
|
### Example usage
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local regex = require("@lune/regex")
|
local regex = require("@lune/regex")
|
||||||
|
|
||||||
local re = regex.new("(?<day>[0-9]{2})-(?<month>[0-9]{2})-(?<year>[0-9]{4})")
|
local re = regex.new("(?<day>[0-9]{2})-(?<month>[0-9]{2})-(?<year>[0-9]{4})")
|
||||||
|
|
||||||
local caps = re:captures("On 14-03-2010, I became a Tenneessee lamb.");
|
local caps = re:captures("On 14-03-2010, I became a Tenneessee lamb.");
|
||||||
assert(caps ~= nil, "Example pattern should match example text")
|
assert(caps ~= nil, "Example pattern should match example text")
|
||||||
|
|
||||||
local formatted = caps:format("year=$year, month=$month, day=$day")
|
local formatted = caps:format("year=$year, month=$month, day=$day")
|
||||||
print(formatted) -- "year=2010, month=03, day=14"
|
print(formatted) -- "year=2010, month=03, day=14"
|
||||||
```
|
```
|
||||||
|
|
||||||
@param format -- The format string to use
|
@param format -- The format string to use
|
||||||
@return string -- The formatted string
|
@return string -- The formatted string
|
||||||
]=]
|
]=]
|
||||||
function RegexCaptures.format(self: RegexCaptures, format: string): string
|
format: (self: RegexCaptures, format: string) -> string,
|
||||||
return nil :: any
|
},
|
||||||
end
|
{} :: {
|
||||||
|
__len: (self: RegexCaptures) -> number,
|
||||||
export type RegexCaptures = typeof(RegexCaptures)
|
}
|
||||||
|
))
|
||||||
|
|
||||||
local Regex = {}
|
local Regex = {}
|
||||||
|
|
||||||
|
|
|
@ -504,4 +504,95 @@ roblox.Instance = (nil :: any) :: {
|
||||||
new: ((className: "DataModel") -> DataModel) & ((className: string) -> Instance),
|
new: ((className: "DataModel") -> DataModel) & ((className: string) -> Instance),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@within Roblox
|
||||||
|
@tag must_use
|
||||||
|
|
||||||
|
Returns the path to the system's Roblox Studio executable.
|
||||||
|
|
||||||
|
There is no guarantee that this will exist, but if Studio is installed this
|
||||||
|
is where it will be.
|
||||||
|
|
||||||
|
### Example usage
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local roblox = require("@lune/roblox")
|
||||||
|
|
||||||
|
local pathToStudio = roblox.studioApplicationPath()
|
||||||
|
print("Studio is located at:", pathToStudio)
|
||||||
|
```
|
||||||
|
]=]
|
||||||
|
function roblox.studioApplicationPath(): string
|
||||||
|
return nil :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@within Roblox
|
||||||
|
@tag must_use
|
||||||
|
|
||||||
|
Returns the path to the `Content` folder of the system's current Studio
|
||||||
|
install.
|
||||||
|
|
||||||
|
This folder will always exist if Studio is installed.
|
||||||
|
|
||||||
|
### Example usage
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local roblox = require("@lune/roblox")
|
||||||
|
|
||||||
|
local pathToContent = roblox.studioContentPath()
|
||||||
|
print("Studio's content folder is located at:", pathToContent)
|
||||||
|
```
|
||||||
|
]=]
|
||||||
|
function roblox.studioContentPath(): string
|
||||||
|
return nil :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@within Roblox
|
||||||
|
@tag must_use
|
||||||
|
|
||||||
|
Returns the path to the `plugin` folder of the system's current Studio
|
||||||
|
install. This is the path where local plugins are installed.
|
||||||
|
|
||||||
|
This folder may not exist if the user has never installed a local plugin.
|
||||||
|
It will also not necessarily take into account custom plugin directories
|
||||||
|
set from Studio.
|
||||||
|
|
||||||
|
### Example usage
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local roblox = require("@lune/roblox")
|
||||||
|
|
||||||
|
local pathToPluginFolder = roblox.studioPluginPath()
|
||||||
|
print("Studio's plugin folder is located at:", pathToPluginFolder)
|
||||||
|
```
|
||||||
|
]=]
|
||||||
|
function roblox.studioPluginPath(): string
|
||||||
|
return nil :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@within Roblox
|
||||||
|
@tag must_use
|
||||||
|
|
||||||
|
Returns the path to the `BuiltInPlugin` folder of the system's current
|
||||||
|
Studio install. This is the path where built-in plugins like the ToolBox
|
||||||
|
are installed.
|
||||||
|
|
||||||
|
This folder will always exist if Studio is installed.
|
||||||
|
|
||||||
|
### Example usage
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local roblox = require("@lune/roblox")
|
||||||
|
|
||||||
|
local pathToPluginFolder = roblox.studioBuiltinPluginPath()
|
||||||
|
print("Studio's built-in plugin folder is located at:", pathToPluginFolder)
|
||||||
|
```
|
||||||
|
]=]
|
||||||
|
function roblox.studioBuiltinPluginPath(): string
|
||||||
|
return nil :: any
|
||||||
|
end
|
||||||
|
|
||||||
return roblox
|
return roblox
|
||||||
|
|
Loading…
Add table
Reference in a new issue