From 694a630ad36be41adc61f738c9e28162ca68e8ce Mon Sep 17 00:00:00 2001 From: cyber01 Date: Tue, 7 Jun 2016 14:51:38 +0300 Subject: [PATCH 1/7] Added saveConfiguration to manual --- .../assets/opencomputers/doc/ru_RU/item/nanomachines.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/assets/opencomputers/doc/ru_RU/item/nanomachines.md b/src/main/resources/assets/opencomputers/doc/ru_RU/item/nanomachines.md index 93e5ab933..60f14016a 100644 --- a/src/main/resources/assets/opencomputers/doc/ru_RU/item/nanomachines.md +++ b/src/main/resources/assets/opencomputers/doc/ru_RU/item/nanomachines.md @@ -27,6 +27,7 @@ - `getInput(index:number)` - Запрос текущего состояния контакта с указанным индексом. - `setInput(index:number, value:boolean)` - Устанавливает указанный контакт в указанное состояние. - `getActiveEffects()` - Запрос списка активных эффектов. Некоторые эффекты могут быть не показаны в этом списке. +- `saveConfiguration()` - Требует наличия в инвентаре нанороботов, сохраняет текущую конфигурацию нанороботов игрока в них. Например, в OpenOS: - `component.modem.broadcast(1, "nanomachines", "setInput", 1, true)` активирует первый контакт. From cf43ca1d8671ad22067c066bbab2b5be6a730e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Thu, 9 Jun 2016 21:33:48 +0200 Subject: [PATCH 2/7] Added `computer.getProgramLocations` and IMC message to populate the returned table. --- src/main/java/li/cil/oc/api/IMC.java | 62 +++++++++++++++---- .../assets/opencomputers/lua/machine.lua | 7 ++- src/main/scala/li/cil/oc/common/IMC.scala | 8 +++ .../opencomputers/ModOpenComputers.scala | 21 +++++++ .../li/cil/oc/server/machine/Machine.scala | 4 ++ .../oc/server/machine/ProgramLocations.scala | 19 ++++++ 6 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 src/main/scala/li/cil/oc/server/machine/ProgramLocations.scala diff --git a/src/main/java/li/cil/oc/api/IMC.java b/src/main/java/li/cil/oc/api/IMC.java index 4cf7a391b..c03b2251d 100644 --- a/src/main/java/li/cil/oc/api/IMC.java +++ b/src/main/java/li/cil/oc/api/IMC.java @@ -4,6 +4,7 @@ import cpw.mods.fml.common.event.FMLInterModComms; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.NBTTagString; import org.apache.commons.lang3.tuple.Pair; /** @@ -20,6 +21,7 @@ import org.apache.commons.lang3.tuple.Pair; * copy this class while keeping the package name, to avoid conflicts if this * class gets updated. */ +@SuppressWarnings("unused") public final class IMC { /** * Register a callback that is used as a filter for assembler templates. @@ -37,7 +39,7 @@ public final class IMC { * * @param callback the callback to register as a filtering method. */ - public static void registerAssemblerFilter(String callback) { + public static void registerAssemblerFilter(final String callback) { FMLInterModComms.sendMessage(MOD_ID, "registerAssemblerFilter", callback); } @@ -98,7 +100,7 @@ public final class IMC { * with only two card slots will pass null * for the third component slot. Up to nine. */ - public static void registerAssemblerTemplate(String name, String select, String validate, String assemble, Class host, int[] containerTiers, int[] upgradeTiers, Iterable> componentSlots) { + public static void registerAssemblerTemplate(final String name, final String select, final String validate, final String assemble, final Class host, final int[] containerTiers, final int[] upgradeTiers, final Iterable> componentSlots) { final NBTTagCompound nbt = new NBTTagCompound(); if (name != null) { nbt.setString("name", name); @@ -187,7 +189,7 @@ public final class IMC { * @param disassemble callback used to apply a template and extract * ingredients from an item. */ - public static void registerDisassemblerTemplate(String name, String select, String disassemble) { + public static void registerDisassemblerTemplate(final String name, final String select, final String disassemble) { final NBTTagCompound nbt = new NBTTagCompound(); if (name != null) { nbt.setString("name", name); @@ -218,7 +220,7 @@ public final class IMC { * * @param callback the callback to register as a durability provider. */ - public static void registerToolDurabilityProvider(String callback) { + public static void registerToolDurabilityProvider(final String callback) { FMLInterModComms.sendMessage(MOD_ID, "registerToolDurabilityProvider", callback); } @@ -234,7 +236,7 @@ public final class IMC { *

* Signature of callbacks must be: *

-     * boolean callback(EntityPlayer player, int x, int y, int z, boolean changeDurability)
+     * boolean callback(EntityPlayer player, BlockPos pos, boolean changeDurability)
      * 
*

* Callbacks must be declared as packagePath.className.methodName. @@ -242,7 +244,7 @@ public final class IMC { * * @param callback the callback to register as a wrench tool handler. */ - public static void registerWrenchTool(String callback) { + public static void registerWrenchTool(final String callback) { FMLInterModComms.sendMessage(MOD_ID, "registerWrenchTool", callback); } @@ -265,7 +267,7 @@ public final class IMC { * * @param callback the callback to register as a wrench tool tester. */ - public static void registerWrenchToolCheck(String callback) { + public static void registerWrenchToolCheck(final String callback) { FMLInterModComms.sendMessage(MOD_ID, "registerWrenchToolCheck", callback); } @@ -291,7 +293,7 @@ public final class IMC { * @param canCharge the callback to register for checking chargeability. * @param charge the callback to register for charging items. */ - public static void registerItemCharge(String name, String canCharge, String charge) { + public static void registerItemCharge(final String name, final String canCharge, final String charge) { final NBTTagCompound nbt = new NBTTagCompound(); nbt.setString("name", name); nbt.setString("canCharge", canCharge); @@ -319,7 +321,7 @@ public final class IMC { * * @param callback the callback to register as an ink provider. */ - public static void registerInkProvider(String callback) { + public static void registerInkProvider(final String callback) { FMLInterModComms.sendMessage(MOD_ID, "registerInkProvider", callback); } @@ -332,7 +334,7 @@ public final class IMC { * * @param peripheral the class of the peripheral to blacklist. */ - public static void blacklistPeripheral(Class peripheral) { + public static void blacklistPeripheral(final Class peripheral) { FMLInterModComms.sendMessage(MOD_ID, "blacklistPeripheral", peripheral.getName()); } @@ -351,7 +353,7 @@ public final class IMC { * @param host the class of the host to blacklist the component for. * @param stack the item stack representing the blacklisted component. */ - public static void blacklistHost(String name, Class host, ItemStack stack) { + public static void blacklistHost(final String name, final Class host, final ItemStack stack) { final NBTTagCompound nbt = new NBTTagCompound(); nbt.setString("name", name); nbt.setString("host", host.getName()); @@ -372,6 +374,44 @@ public final class IMC { FMLInterModComms.sendMessage(MOD_ID, "registerCustomPowerSystem", "true"); } + /** + * Register a mapping of program name to loot disk. + *

+ * The table of mappings is made available to machines to allow displaying + * a message to the user telling her on which floppy disk to find the program + * they were trying to run. + *

+ * For Lua programs, this should be the program name, i.e. the file + * name without the .lua extension. + *

+ * The list of architectures is optional, if it is not specified this mapping + * will be made available to all architectures. It allows filtering since + * typically programs will be written for one specific architecture type, e.g. + * Lua programs will not (directly) work on a MIPS architecture. The name + * specified is the in the {@link li.cil.oc.api.machine.Architecture.Name} + * annotation of the architecture (also shown in the CPU tooltip). + *

+ * The architecture names for Lua are Lua 5.2, Lua 5.3 + * and LuaJ for example. + * + * @param programName the name of the program. + * @param diskLabel the label of the disk the program is on. + * @param architectures the names of the architectures this entry applies to. + */ + public static void registerProgramDiskLabel(final String programName, final String diskLabel, final String... architectures) { + final NBTTagCompound nbt = new NBTTagCompound(); + nbt.setString("program", programName); + nbt.setString("label", diskLabel); + if (architectures != null && architectures.length > 0) { + final NBTTagList architecturesNbt = new NBTTagList(); + for (final String architecture : architectures) { + architecturesNbt.appendTag(new NBTTagString(architecture)); + } + nbt.setTag("architectures", architecturesNbt); + } + FMLInterModComms.sendMessage(MOD_ID, "registerProgramDiskLabel", nbt); + } + // ----------------------------------------------------------------------- // private static final String MOD_ID = "OpenComputers"; diff --git a/src/main/resources/assets/opencomputers/lua/machine.lua b/src/main/resources/assets/opencomputers/lua/machine.lua index 23b3e45b1..3a67047dd 100644 --- a/src/main/resources/assets/opencomputers/lua/machine.lua +++ b/src/main/resources/assets/opencomputers/lua/machine.lua @@ -1334,8 +1334,11 @@ local libcomputer = { beep = function(...) return libcomponent.invoke(computer.address(), "beep", ...) end, - getDeviceInfo = function(...) - return libcomponent.invoke(computer.address(), "getDeviceInfo", ...) + getDeviceInfo = function() + return libcomponent.invoke(computer.address(), "getDeviceInfo") + end, + getProgramLocations = function() + return libcomponent.invoke(computer.address(), "getProgramLocations") end, getArchitectures = function(...) diff --git a/src/main/scala/li/cil/oc/common/IMC.scala b/src/main/scala/li/cil/oc/common/IMC.scala index 7cdd67891..27811148c 100644 --- a/src/main/scala/li/cil/oc/common/IMC.scala +++ b/src/main/scala/li/cil/oc/common/IMC.scala @@ -12,8 +12,11 @@ import li.cil.oc.common.template.DisassemblerTemplates import li.cil.oc.integration.util.ItemCharge import li.cil.oc.integration.util.Wrench import li.cil.oc.server.driver.Registry +import li.cil.oc.server.machine.ProgramLocations +import li.cil.oc.util.ExtendedNBT._ import net.minecraft.entity.player.EntityPlayer import net.minecraft.item.ItemStack +import net.minecraft.nbt.NBTTagString import net.minecraftforge.common.util.Constants.NBT import scala.collection.convert.WrapAsScala._ @@ -91,8 +94,13 @@ object IMC { } } else if (message.key == "registerCustomPowerSystem" && message.isStringMessage) { + OpenComputers.log.info(s"Was told there is an unknown power system present by mod ${message.getSender}.") Settings.get.is3rdPartyPowerSystemPresent = message.getStringValue == "true" } + else if (message.key == "registerProgramDiskLabel" && message.isNBTMessage) { + OpenComputers.log.info(s"Registering new program location mapping for program '${message.getNBTValue.getString("program")}' being on disk '${message.getNBTValue.getString("label")}' from mod ${message.getSender}.") + ProgramLocations.addMapping(message.getNBTValue.getString("program"), message.getNBTValue.getString("label"), message.getNBTValue.getTagList("architectures", NBT.TAG_STRING).map((tag: NBTTagString) => tag.func_150285_a_()).toArray: _*) + } else { OpenComputers.log.warn(s"Got an unrecognized or invalid IMC message '${message.key}' from mod ${message.getSender}.") } diff --git a/src/main/scala/li/cil/oc/integration/opencomputers/ModOpenComputers.scala b/src/main/scala/li/cil/oc/integration/opencomputers/ModOpenComputers.scala index d415189a9..097e93f5d 100644 --- a/src/main/scala/li/cil/oc/integration/opencomputers/ModOpenComputers.scala +++ b/src/main/scala/li/cil/oc/integration/opencomputers/ModOpenComputers.scala @@ -67,9 +67,30 @@ object ModOpenComputers extends ModProxy { "OpenComputers", "li.cil.oc.integration.opencomputers.ModOpenComputers.canCharge", "li.cil.oc.integration.opencomputers.ModOpenComputers.charge") + api.IMC.registerInkProvider("li.cil.oc.integration.opencomputers.ModOpenComputers.inkCartridgeInkProvider") api.IMC.registerInkProvider("li.cil.oc.integration.opencomputers.ModOpenComputers.dyeInkProvider") + api.IMC.registerProgramDiskLabel("build", "builder", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("dig", "dig", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("base64", "data", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("deflate", "data", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("gpg", "data", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("inflate", "data", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("md5sum", "data", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("sha256sum", "data", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("refuel", "generator", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("pastebin", "internet", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("wget", "internet", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("irc", "irc", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("maze", "maze", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("arp", "network", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("ifconfig", "network", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("ping", "network", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("route", "network", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("opl-flash", "openloader", "Lua 5.2", "Lua 5.3", "LuaJ") + api.IMC.registerProgramDiskLabel("oppm", "oppm", "Lua 5.2", "Lua 5.3", "LuaJ") + ForgeChunkManager.setForcedChunkLoadingCallback(OpenComputers, ChunkloaderUpgradeHandler) FMLCommonHandler.instance.bus.register(EventHandler) diff --git a/src/main/scala/li/cil/oc/server/machine/Machine.scala b/src/main/scala/li/cil/oc/server/machine/Machine.scala index 0a04b298e..8aaacc6ef 100644 --- a/src/main/scala/li/cil/oc/server/machine/Machine.scala +++ b/src/main/scala/li/cil/oc/server/machine/Machine.scala @@ -452,6 +452,10 @@ class Machine(val host: MachineHost) extends prefab.ManagedEnvironment with mach }.collect { case Some(kvp) => kvp }.toMap) } + @Callback(doc = """function():table -- Returns a map of program name to disk label for known programs.""") + def getProgramLocations(context: Context, args: Arguments): Array[AnyRef] = + result(ProgramLocations.getMappings(Machine.getArchitectureName(architecture.getClass))) + // ----------------------------------------------------------------------- // def isExecuting = state.synchronized(state.contains(Machine.State.Running)) diff --git a/src/main/scala/li/cil/oc/server/machine/ProgramLocations.scala b/src/main/scala/li/cil/oc/server/machine/ProgramLocations.scala new file mode 100644 index 000000000..d4578a61c --- /dev/null +++ b/src/main/scala/li/cil/oc/server/machine/ProgramLocations.scala @@ -0,0 +1,19 @@ +package li.cil.oc.server.machine + +import scala.collection.mutable + +object ProgramLocations { + final val architectureLocations = mutable.Map.empty[String, mutable.Map[String, String]] + final val globalLocations = mutable.Map.empty[String, String] + + def addMapping(program: String, label: String, architectures: String*): Unit = { + if (architectures == null || architectures.isEmpty) { + globalLocations += (program -> label) + } + else { + architectures.foreach(architectureLocations.getOrElseUpdate(_, mutable.Map.empty[String, String]) += (program -> label)) + } + } + + def getMappings(architecture: String) = architectureLocations.getOrElse(architecture, Iterable.empty) ++ globalLocations +} From 3e86a3278384e1f294bfa40fac2ca9fd87aeccca Mon Sep 17 00:00:00 2001 From: payonel Date: Thu, 9 Jun 2016 06:56:18 -0700 Subject: [PATCH 3/7] openos 1.6 release candidate cat: default to stdin with no args cp: support "contents of" /. paths, correct verbose output, prefer link copy over directory, error on missing source files df: support relative paths guid: correct guid format (8-4-4-4-12) head: close stdin install: support major rework to search for candidates filesystems and support .lootprop feature, new manual file less: new pager option, supports scrollback and mouse scroll ln: use source name when omitted mv: report error when target path is read only, and report error when source path is a mount point profile: remove less alias (we have /bin/less now) rm: fix issue where links to dirs could not be removed serialization: inline pairs in case of [C] boundary issue with delay loaded libraries /lib/sh: support white space in tab completion with back slashes, emulate SIGPIPE on closed pipe writes, shell: allow calls to getWorkingDirectory during boot before PWD is set (defaults to /) term: fix to not consume(hide) hard interrupt in term.pull, and new method term.scroll for vertical scrolling text: advanced internal tokenizer upgrade to more fully support whitespace metadata needs of the shell transforms: new api, at(tbl, index), to return key-value pairs by index other files: fix formatting and typos --- .../opencomputers/loot/openos/bin/cat.lua | 43 ++- .../opencomputers/loot/openos/bin/cp.lua | 38 ++- .../opencomputers/loot/openos/bin/df.lua | 2 +- .../opencomputers/loot/openos/bin/head.lua | 4 +- .../opencomputers/loot/openos/bin/install.lua | 271 +++++++++++++--- .../opencomputers/loot/openos/bin/less.lua | 300 ++++++++++++++++++ .../opencomputers/loot/openos/bin/ln.lua | 4 + .../opencomputers/loot/openos/bin/mv.lua | 25 ++ .../opencomputers/loot/openos/bin/rm.lua | 2 +- .../opencomputers/loot/openos/etc/profile | 1 - .../opencomputers/loot/openos/lib/guid.lua | 2 +- .../loot/openos/lib/serialization.lua | 10 +- .../opencomputers/loot/openos/lib/sh.lua | 131 +++++--- .../opencomputers/loot/openos/lib/shell.lua | 2 +- .../opencomputers/loot/openos/lib/term.lua | 45 ++- .../opencomputers/loot/openos/lib/text.lua | 45 +-- .../loot/openos/lib/transforms.lua | 35 +- .../opencomputers/loot/openos/usr/man/head | 38 +-- .../opencomputers/loot/openos/usr/man/install | 81 +++++ 19 files changed, 891 insertions(+), 188 deletions(-) create mode 100644 src/main/resources/assets/opencomputers/loot/openos/bin/less.lua create mode 100644 src/main/resources/assets/opencomputers/loot/openos/usr/man/install diff --git a/src/main/resources/assets/opencomputers/loot/openos/bin/cat.lua b/src/main/resources/assets/opencomputers/loot/openos/bin/cat.lua index f977d0eec..8772e44e4 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/bin/cat.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/bin/cat.lua @@ -4,32 +4,27 @@ local fs = require("filesystem") local args = shell.parse(...) local ec = 0 if #args == 0 then - repeat - local read = io.read("*L") - if read then - io.write(read) - end - until not read -else - for i = 1, #args do - local arg = args[i] - if fs.isDirectory(arg) then - io.stderr:write(string.format('cat %s: Is a directory\n', arg)) + args = {"-"} +end + +for i = 1, #args do + local arg = args[i] + if fs.isDirectory(arg) then + io.stderr:write(string.format('cat %s: Is a directory\n', arg)) + ec = 1 + else + local file, reason = args[i] == "-" and io.stdin or io.open(shell.resolve(args[i])) + if not file then + io.stderr:write(string.format("cat: %s: %s\n",args[i],tostring(reason))) ec = 1 else - local file, reason = args[i] == "-" and io.stdin or io.open(shell.resolve(args[i])) - if not file then - io.stderr:write(string.format("cat: %s: %s\n",args[i],tostring(reason))) - ec = 1 - else - repeat - local line = file:read("*L") - if line then - io.write(line) - end - until not line - file:close() - end + repeat + local line = file:read("*L") + if line then + io.write(line) + end + until not line + file:close() end end end diff --git a/src/main/resources/assets/opencomputers/loot/openos/bin/cp.lua b/src/main/resources/assets/opencomputers/loot/openos/bin/cp.lua index 9caba103b..f805d5ef9 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/bin/cp.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/bin/cp.lua @@ -15,14 +15,9 @@ if #args < 2 then return 1 end +local exit_code = nil options.P = options.P or options.r -local from = {} -for i = 1, #args - 1 do - table.insert(from, shell.resolve(args[i])) -end -local to = shell.resolve(args[#args]) - local function status(from, to) if options.v then io.write(from .. " -> " .. to .. "\n") @@ -68,14 +63,14 @@ for dev,path in fs.mounts() do end local function recurse(fromPath, toPath, origin) - status(fromPath, toPath) local isLink, target = fs.isLink(fromPath) if isLink and options.P then + status(fromPath, toPath) return fs.link(target, toPath) - end - if fs.isDirectory(fromPath) then + elseif fs.isDirectory(fromPath) then if not options.r then io.write("omitting directory `" .. fromPath .. "'\n") + exit_code = 1 return true end if fs.exists(toPath) and not fs.isDirectory(toPath) then @@ -85,10 +80,13 @@ local function recurse(fromPath, toPath, origin) if options.x and origin and mounts[fs.canonical(fromPath)] then return true end - if fs.get(fromPath) == fs.get(toPath) and fs.canonical(fs.path(toPath)):find(fs.canonical(fromPath),1,true) then + if fs.get(fromPath) == fs.get(toPath) and fs.canonical(toPath):find(fs.canonical(fromPath),1,true) then return nil, "cannot copy a directory, `" .. fromPath .. "', into itself, `" .. toPath .. "'" end - fs.makeDirectory(toPath) + if not fs.exists(toPath) then + status(fromPath, toPath) + fs.makeDirectory(toPath) + end for file in fs.list(fromPath) do local result, reason = recurse(fs.concat(fromPath, file), fs.concat(toPath, file), origin or fs.get(fromPath)) if not result then @@ -96,7 +94,7 @@ local function recurse(fromPath, toPath, origin) end end return true - else + elseif fs.exists(fromPath) then if fs.exists(toPath) then if fs.canonical(fromPath) == fs.canonical(toPath) then return nil, "`" .. fromPath .. "' and `" .. toPath .. "' are the same file" @@ -128,12 +126,22 @@ local function recurse(fromPath, toPath, origin) end fs.remove(toPath) end + status(fromPath, toPath) return fs.copy(fromPath, toPath) + else + return nil, "`" .. fromPath .. "': No such file or directory" end end -for _, fromPath in ipairs(from) do + +local to = shell.resolve(args[#args]) + +for i = 1, #args - 1 do + local fromPath, cuts = args[i]:gsub("(/%.%.?)$", "%1") + fromPath = shell.resolve(fromPath) local toPath = to - if fs.isDirectory(toPath) then + -- fromPath ending with /. indicates copying the contents of fromPath + -- in which case (cuts>0) we do not append fromPath name to toPath + if cuts == 0 and fs.isDirectory(toPath) then toPath = fs.concat(toPath, fs.name(fromPath)) end result, reason = recurse(fromPath, toPath) @@ -144,3 +152,5 @@ for _, fromPath in ipairs(from) do return 1 end end + +return exit_code diff --git a/src/main/resources/assets/opencomputers/loot/openos/bin/df.lua b/src/main/resources/assets/opencomputers/loot/openos/bin/df.lua index 998b08c75..975e37f3d 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/bin/df.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/bin/df.lua @@ -27,7 +27,7 @@ if #args == 0 then end else for i = 1, #args do - local proxy, path = fs.get(args[i]) + local proxy, path = fs.get(shell.resolve(args[i])) if not proxy then io.stderr:write(args[i], ": no such file or directory\n") else diff --git a/src/main/resources/assets/opencomputers/loot/openos/bin/head.lua b/src/main/resources/assets/opencomputers/loot/openos/bin/head.lua index 2dfd22631..ea731f89b 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/bin/head.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/bin/head.lua @@ -110,7 +110,7 @@ for i=1,#args do local file if arg == '-' then arg = 'standard input' - file = setmetatable({close=function()end},{__index=io.stdin}) + file = io.stdin else file, reason = io.open(arg, 'r') if not file then @@ -119,7 +119,7 @@ for i=1,#args do end if file then if verbose or #args > 1 then - io.write(string.format('==> %s <==', arg)) + io.write(string.format('==> %s <==\n', arg)) end local stream = new_stream() diff --git a/src/main/resources/assets/opencomputers/loot/openos/bin/install.lua b/src/main/resources/assets/opencomputers/loot/openos/bin/install.lua index 940eddbd3..6d3b1f990 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/bin/install.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/bin/install.lua @@ -1,80 +1,255 @@ local component = require("component") local computer = require("computer") local event = require("event") -local filesystem = require("filesystem") +local fs = require("filesystem") local unicode = require("unicode") local shell = require("shell") +local tx = require("transforms") +local text = require("text") local args, options = shell.parse(...) -local fromAddress = options.from and component.get(options.from) or filesystem.get(os.getenv("_")).address -local candidates = {} -for address in component.list("filesystem", true) do - local dev = component.proxy(address) - if not dev.isReadOnly() and dev.address ~= computer.tmpAddress() and dev.address ~= fromAddress then - table.insert(candidates, dev) - end +local sources = {} +local targets = {} + +if options.help then + print([[Usage: install [OPTION]... + --from=ADDR install filesystem at ADDR + default: builds list of + candidates and prompts user + --to=ADDR same as --from but for target + --fromDir=PATH install PATH from source + --root=PATH same as --fromDir but target + --toDir=PATH same as --root + -u, --update update files interactively +The following only pertain when .osprop exists + --nolabelset do not label target + --name override label from .osprop + --noboot do not use target for boot + --noreboot do not reboot after install]]) + return nil -- exit success end -if #candidates == 0 then - io.stderr:write("No writable disks found, aborting.\n") +local rootfs = fs.get("/") +if not rootfs then + io.stderr:write("no root filesystem, aborting\n"); return 1 end -for i = 1, #candidates do - local label = candidates[i].getLabel() - if label then - label = label .. " (" .. candidates[i].address:sub(1, 8) .. "...)" - else - label = candidates[i].address - end - io.write(i .. ") " .. label .. "\n") +local rootAddress = rootfs.address +-- if the rootfs is read only, it is probably the loot disk! +local rootException = rootAddress +if rootfs.isReadOnly() then + rootException = nil end -io.write("To select the device to install to, please enter a number between 1 and " .. #candidates .. ".\n") -io.write("Press 'q' to cancel the installation.\n") -local choice -while not choice do - result = io.read() - if result:sub(1, 1):lower() == "q" then - os.exit() +-- this may be OpenOS specific, default to "" in case no /dev mount point +local devfsAddress = (fs.get("/dev/") or {}).address or "" + +-- tmp is only valid if specified as an option +local tmpAddress = computer.tmpAddress() + +local fromAddress = options.from +local toAddress = options.to +local fromDir = (options.fromDir or "") .. '/.' +local root = (options.root or options.toDir or "") .. "/." +options.update = options.u or options.update + +local function cleanPath(path) + if path then + local rpath = shell.resolve(path) + if fs.isDirectory(rpath) then + return fs.canonical(rpath):gsub("/+$", "") .. '/' + end end - local number = tonumber(result) - if number and number > 0 and number <= #candidates then - choice = candidates[number] + return path +end + +fromAddress = cleanPath(fromAddress) +toAddress = cleanPath(toAddress) + +local function validDevice(candidate, exceptions, specified, existing) + local address = candidate.dev.address + + if tx.first(existing, function(e) return e.dev.address == address end) then + return + end + + local path = candidate.path + if specified then + return address:find(specified, 1, true) == 1 or specified == path else - io.write("Invalid input, please try again.\n") + return not tx.find(exceptions, {address}) end end -local function findMount(address) - for fs, path in filesystem.mounts() do - if fs.address == component.get(address) then - return path +-- use a single for loop of all filesystems to build the list of candidates of sources and targets +for dev, path in fs.mounts() do + local candidate = {dev=dev, path=path} + + if validDevice(candidate, {devfsAddress, tmpAddress, rootException}, fromAddress, sources) then + if fromAddress or fs.list(path)() then + table.insert(sources, candidate) + end + end + + if validDevice(candidate, {devfsAddress, tmpAddress}, toAddress, targets) then + if not dev.isReadOnly() then + table.insert(targets, candidate) + elseif toAddress then + io.stderr:write("Cannot install to " .. toAddress .. ", it is read only\n") + return 1 end end end -local name = options.name or "OpenOS" -io.write("Installing " .. name .." to device " .. (choice.getLabel() or choice.address) .. "\n") +if fromAddress and #sources == 0 then + io.stderr:write("No such filesystem to install from: " .. fromAddress .. "\n") + return 1 +end + +if #targets == 0 then + if toAddress then + io.stderr:write("No such filesystem to install to: " .. toAddress .. "\n") + else + io.stderr:write("No writable disks found, aborting\n") + end + return 1 +end + +----- For now, I am allowing source==target -- cp can handle it if the user prepares conditions correctly +----- in other words, install doesn't need to filter this scenario: +--if #targets == 1 and #sources == 1 and targets[1] == sources[1] then +-- io.stderr:write("It is not the intent of install to use the same source and target filesystem.\n") +-- return 1 +--end + +local function prompt_select(devs, direction) + + local choice = devs[1] + if #devs > 1 then + print("Select the device to install " .. direction) + + for i = 1, #devs do + local src = devs[i] + local label = src.dev.getLabel() + if label then + label = label .. " (" .. src.dev.address:sub(1, 8) .. "...)" + else + label = src.dev.address + end + print(i .. ") " .. label .. " at " .. src.path) + end + + print("Please enter a number between 1 and " .. #devs) + io.write("Enter 'q' to cancel the installation: ") + local choice + while not choice do + result = io.read() + if result:sub(1, 1):lower() == "q" then + os.exit() + end + local number = tonumber(result) + if number and number > 0 and number <= #devs then + choice = devs[number] + else + io.write("Invalid input, please try again: ") + end + end + end + + choice.display = (choice.path == '/' and "the root filesystem") or choice.dev.getLabel() or choice.path + + if #devs == 1 then + print("Selecting " .. choice.display .. " (only option)") + end + + return choice +end + +table.sort(sources, function(a, b) return a.path 1 then + io.write("Usage: more \n") + io.write("- or no args reads stdin\n") + return 1 +end +local arg = args[1] or "-" + +local initial_offset + +-- test validity of args +do + if arg == "-" then + if not io.stdin then + io.stderr:write("this process has no stdin\n") + return 1 + end + -- stdin may not be core_stdin + initial_offset = io.stdin:seek("cur") + else + local file, reason = io.open(shell.resolve(arg)) + if not file then + io.stderr:write(reason,'\n') + return 1 + end + initial_offset = file:seek("cur") + file:close() + end +end + +local width, height = term.getViewport() +local max_display = height - 1 + +-- mgr is the data manager, it keeps track of what has been loaded +-- keeps a reasonable buffer, and keeps track of file handles +local mgr +mgr = +{ + lines = {}, -- current buffer + chunk, -- temp from last read line that hasn't finished wrapping + lines_released = 0, + can_seek = initial_offset, + capacity = math.max(1, math.min(max_display * 10, computer.freeMemory() / 2 / width)), + size = 0, + file = nil, + path = arg ~= "-" and shell.resolve(arg) or nil, + open = function() + mgr.file = mgr.path and io.open(mgr.path) or io.stdin + end, + top_of_file = max_display, + total_lines = nil, -- nil means unknown + latest_line = nil, -- used for status improvements + rollback = function() + if not mgr.can_seek then + return false + end + if not mgr.file then + mgr.open() + elseif not mgr.file:seek("set", 0) then + mgr.close() + return false + end + mgr.lines_released = 0 + mgr.lines = {} + mgr.size = 0 + return true + end, + at = function(line_number) + local index = line_number - mgr.lines_released + if index < 1 then + index = index + mgr.capacity + if #mgr.lines ~= mgr.capacity or index <= mgr.size then + return nil + end + elseif index > mgr.size then + return nil + end + return mgr.lines[index] -- cached + end, + load = function(line_number) + local index = line_number - mgr.lines_released + if mgr.total_lines and mgr.total_lines < line_number then + return nil + end + if mgr.at(line_number) then + return true + end + -- lines[index] is line (lines_released + index) in the file + -- thus index == line_number - lines_released + if index <= 0 then + -- we have previously freed some of the buffer, and now the user wants it back + if not mgr.rollback() then + -- TODO how to nicely fail if can_seek == false + -- or if no more buffers + error("cannot load prior data") + end + return mgr.load(line_number) -- retry + end + if mgr.read_next() then + return mgr.load(line_number) -- retry + end + -- ran out of file, could not reach line_number + end, + write = function(line_number) + local line = mgr.at(line_number) + if not line then return false end + term.write(line) + end, + close = function() + if mgr.file then + mgr.file:close() + mgr.file = nil + end + end, + last = function() + -- return the last line_number available right now in the cache + return mgr.size + mgr.lines_released + end, + check_capacity = function(release) + -- if we have reached capacity + if mgr.size >= mgr.capacity then + if release then + mgr.lines_released = mgr.lines_released + mgr.size + mgr.size = 0 + end + return true + end + end, + insert = function(line) + if mgr.check_capacity() then return false end + mgr.size = mgr.size + 1 + mgr.lines[mgr.size] = line + -- latest_line is not used for computation, just for status reports + mgr.latest_line = math.max(mgr.latest_line or 0, mgr.size + mgr.lines_released) + return true + end, + read_next = function() + -- total_lines indicates we've reached the end previously + -- but have we just prior to this reached the end? + if mgr.last() == mgr.total_lines then + -- then there is no more after that point + return nil + end + if not mgr.file then + mgr.open() + end + mgr.check_capacity(true) + if not mgr.chunk then + mgr.chunk = mgr.file:read("*l") + if not mgr.chunk then + mgr.total_lines = mgr.size + mgr.lines_released -- now file length is known + mgr.close() + end + end + while mgr.chunk do + local wrapped, next = text.wrap(text.detab(mgr.chunk), width, width) + -- insert fails if capacity is full + if not mgr.insert(wrapped) then + return mgr.last() + end + mgr.chunk = next + end + + return mgr.last() + end, + scroll = function(num) + if num < 0 then + num = math.max(num, mgr.top_of_file) + if num >= 0 then + return true -- nothing to scroll + end + end + + term.setCursor(1, height) + local y = height + term.clearLine() + + if num < 0 then + term.scroll(num) -- push text down + mgr.top_of_file = mgr.top_of_file - num + y = 1 + term.setCursor(1, y) -- ready to write lines above + num = -num -- now print forward + end + + local range + while num > 0 do + -- trigger load of data if needed + local line_number = y - mgr.top_of_file + + if not mgr.load(line_number) then -- nothing more to read from the file + return range ~= nil -- first time it is nil + end + + -- print num range of what is available, scroll to show it (if bottom of screen) + range = math.min(num, mgr.last() - line_number + 1) + + if y == height then + range = math.min(range, max_display) + term.scroll(range) + y = y - range + term.setCursor(1, y) + mgr.top_of_file = mgr.top_of_file - range + end + + for i=1,range do + mgr.write(line_number + i - 1) + term.setCursor(1, y + i) + end + y = y + range + + num = num - range + end + + return true + end, + print_status = function() + local first = mgr.top_of_file >= 1 and 1 or 1 - mgr.top_of_file + local perc = not mgr.total_lines and "--" or tostring((max_display - mgr.top_of_file) / mgr.total_lines * 100):gsub("%..*","") + local last_plus = mgr.total_lines and "" or "+" + local status = string.format("%s lines %d-%d/%s %s%%", mgr.path or "-", first, max_display - mgr.top_of_file, tostring(mgr.total_lines or mgr.latest_line)..last_plus, perc) + + local gpu = term.gpu() + local sf, sb = gpu.setForeground, gpu.setBackground + local b_color, b_is_palette = gpu.getBackground() + local f_color, f_is_palette = gpu.getForeground() + sf(b_color, b_is_palette) + sb(f_color, f_is_palette) + term.write(status) + sb(b_color, b_is_palette) + sf(f_color, f_is_palette) + end +} + +local function update(num) + -- unexpected + if num == 0 then + return + end + + -- if this a positive direction, and we didn't previously know this was the end of the stream, give the user a once chance + local end_is_known = mgr.total_lines + -- clear buttom line, for status + local ok = mgr.scroll(num or max_display) + + -- print status + term.setCursor(1, height) + -- we have to clear again in case we scrolled up + term.clearLine() + mgr.print_status() + return not end_is_known or ok +end + +if not update() then + return +end + +while true do + local ename, address, char, code, dy = term.pull() + local num + if ename == "scroll" then + if dy < 0 then + num = 3 + else + num = -3 + end + elseif ename == "key_down" then + num = 0 + if code == keyboard.keys.q or code == keyboard.keys.d and keyboard.isControlDown() then + break + elseif code == keyboard.keys.space or code == keyboard.keys.pageDown then + num = nil + elseif code == keyboard.keys.pageUp then + num = -max_display + elseif code == keyboard.keys.enter or code == keyboard.keys.down then + num = 1 + elseif code == keyboard.keys.up then + num = -1 + elseif code == keyboard.keys.home then + num = -math.huge + elseif code == keyboard.keys["end"] then + num = math.huge + end + elseif ename == "interrupted" then + break + end + if num then + update(num) + end +end + +term.clearLine() diff --git a/src/main/resources/assets/opencomputers/loot/openos/bin/ln.lua b/src/main/resources/assets/opencomputers/loot/openos/bin/ln.lua index 9076cf511..029b83d63 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/bin/ln.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/bin/ln.lua @@ -16,6 +16,10 @@ else linkpath = fs.concat(shell.getWorkingDirectory(), fs.name(target)) end +if fs.isDirectory(linkpath) then + linkpath = fs.concat(linkpath, fs.name(target)) +end + local result, reason = fs.link(target, linkpath) if not result then io.stderr:write(reason..'\n') diff --git a/src/main/resources/assets/opencomputers/loot/openos/bin/mv.lua b/src/main/resources/assets/opencomputers/loot/openos/bin/mv.lua index 74dacce97..616c41a17 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/bin/mv.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/bin/mv.lua @@ -9,8 +9,33 @@ if #args < 2 then return 1 end +local function is_mount(path) + if not fs.isDirectory(path) then return false end + path = fs.canonical(path) .. '/' + for driver, mount_point in fs.mounts() do + if path == mount_point then + return true + end + end +end + +local function is_readonly(path) + return fs.get(path).isReadOnly() +end + local from = shell.resolve(args[1]) local to = shell.resolve(args[2]) + +if is_readonly(to) then + io.stderr:write("cannot write to " .. to .. ", filesystem is readonly\n"); + return 1 +end + +if is_mount(from) then + io.stderr:write("cannot move " .. from .. ", it is a mount point\n"); + return 1 +end + if fs.isDirectory(to) then to = to .. "/" .. fs.name(from) end diff --git a/src/main/resources/assets/opencomputers/loot/openos/bin/rm.lua b/src/main/resources/assets/opencomputers/loot/openos/bin/rm.lua index e45b268ed..60d38cb5d 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/bin/rm.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/bin/rm.lua @@ -32,7 +32,7 @@ local metas = {} local function _path(m) return shell.resolve(m.rel) end local function _link(m) return fs.isLink(_path(m)) end local function _exists(m) return _link(m) or fs.exists(_path(m)) end -local function _dir(m) return fs.isDirectory(_path(m)) end +local function _dir(m) return not _link(m) and fs.isDirectory(_path(m)) end local function _readonly(m) return not _exists(m) or fs.get(_path(m)).isReadOnly() end local function _empty(m) return _exists(m) and _dir(m) and (fs.list(_path(m))==nil) end diff --git a/src/main/resources/assets/opencomputers/loot/openos/etc/profile b/src/main/resources/assets/opencomputers/loot/openos/etc/profile index 3bfbe6c21..b11d98d5d 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/etc/profile +++ b/src/main/resources/assets/opencomputers/loot/openos/etc/profile @@ -6,7 +6,6 @@ alias copy=cp alias del=rm alias md=mkdir alias cls=clear -alias less=more alias rs=redstone alias view=edit\ -r alias help=man diff --git a/src/main/resources/assets/opencomputers/loot/openos/lib/guid.lua b/src/main/resources/assets/opencomputers/loot/openos/lib/guid.lua index 0fd5245f5..13cfc7217 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/lib/guid.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/lib/guid.lua @@ -28,7 +28,7 @@ end function guid.next() -- e.g. 3c44c8a9-0613-46a2-ad33-97b6ba2e9d9a -- 8-4-4-4-12 - local sets = {8, 4, 4, 12} + local sets = {8, 4, 4, 4, 12} local result = "" local i diff --git a/src/main/resources/assets/opencomputers/loot/openos/lib/serialization.lua b/src/main/resources/assets/opencomputers/loot/openos/lib/serialization.lua index 6aaa05d0c..6864afffb 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/lib/serialization.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/lib/serialization.lua @@ -1,5 +1,11 @@ local serialization = {} +-- delay loaded tables fail to deserialize cross [C] boundaries (such as when having to read files that cause yields) +local local_pairs = function(tbl) + local mt = getmetatable(tbl) + return (mt and mt.__pairs or pairs)(tbl) +end + -- Important: pretty formatting will allow presenting non-serializable values -- but may generate output that cannot be unserialized back. function serialization.serialize(value, pretty) @@ -44,7 +50,7 @@ function serialization.serialize(value, pretty) local f if pretty then local ks, sks, oks = {}, {}, {} - for k in pairs(v) do + for k in local_pairs(v) do if type(k) == "number" then table.insert(ks, k) elseif type(k) == "string" then @@ -72,7 +78,7 @@ function serialization.serialize(value, pretty) end end) else - f = table.pack(pairs(v)) + f = table.pack(local_pairs(v)) end for k, v in table.unpack(f) do if r then diff --git a/src/main/resources/assets/opencomputers/loot/openos/lib/sh.lua b/src/main/resources/assets/opencomputers/loot/openos/lib/sh.lua index 5da1ef026..1befe12da 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/lib/sh.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/lib/sh.lua @@ -217,9 +217,14 @@ function sh.internal.parseCommand(words) table.insert(evaluated_words, arg) end end - local program, reason = shell.resolve(evaluated_words[1], "lua") + local eword = evaluated_words[1] + local possible_dir_path = shell.resolve(eword) + if possible_dir_path and fs.isDirectory(possible_dir_path) then + return nil, string.format("%s: is a directory", eword) + end + local program, reason = shell.resolve(eword, "lua") if not program then - return nil, evaluated_words[1] .. ": " .. reason + return nil, eword .. ": " .. reason end evaluated_words = tx.sub(evaluated_words, 2) return program, evaluated_words @@ -420,6 +425,10 @@ function --[[@delayloaded-start@]] sh.internal.buildPipeChain(threads) if i < #threads then pipe = require("buffer").new("rw", sh.internal.newMemoryStream()) pipe:setvbuf("no") + -- buffer close flushes the buffer, but we have no buffer + -- also, when the buffer is closed, read and writes don't pass through + -- simply put, we don't want buffer:close + pipe.close = function(self) self.stream:close() end pipe.stream.redirect[1] = rawget(pio, 1) pio[1] = pipe table.insert(data.handles, pipe) @@ -507,7 +516,12 @@ function --[[@delayloaded-start@]] sh.getMatchingPrograms(baseName) return result end --[[@delayloaded-end@]] -function --[[@delayloaded-start@]] sh.getMatchingFiles(basePath, name) +function --[[@delayloaded-start@]] sh.getMatchingFiles(partial_path) + -- name: text of the partial file name being expanded + local name = partial_path:gsub("^.*/", "") + -- here we remove the name text from the partialPrefix + local basePath = unicode.sub(partial_path, 1, -unicode.len(name) - 1) + local resolvedPath = shell.resolve(basePath) local result, baseName = {} @@ -525,7 +539,7 @@ function --[[@delayloaded-start@]] sh.getMatchingFiles(basePath, name) for file in fs.list(resolvedPath) do local match = file:match(baseName) if match then - table.insert(result, basePath .. match) + table.insert(result, basePath .. match:gsub("(%s)", "\\%1")) end end -- (cont.) but if there's only one match and it's a directory, *then* we @@ -537,19 +551,57 @@ function --[[@delayloaded-start@]] sh.getMatchingFiles(basePath, name) end --[[@delayloaded-end@]] function --[[@delayloaded-start@]] sh.internal.hintHandlerSplit(line) - if line:sub(-1):find("%s") then - return '', line - end - local splits = text.internal.tokenize(line) + -- I do not plan on having text tokenizer parse error on + -- trailiing \ in case of future support for multiple line + -- input. But, there are also no hints for it + if line:match("\\$") then return nil end + + local splits, simple = text.internal.tokenize(line,{show_escapes=true}) if not splits then -- parse error, e.g. unclosed quotes return nil -- no split, no hints end + local num_splits = #splits - if num_splits == 1 or not isWordOf(splits[num_splits-1],{";","&&","||","|"}) then - return '', line + + -- search for last statement delimiters + local last_close = 0 + for index = num_splits, 1, -1 do + local word = splits[index] + if isWordOf(word, {";","&&","||","|"}) then + last_close = index + break + end + end + + -- if the very last word of the line is a delimiter + -- consider this a fresh new, empty line + -- this captures edge cases with empty input as well (i.e. no splits) + if last_close == num_splits then + return nil -- no hints on empty command + end + + local last_word = splits[num_splits] + local normal = text.internal.normalize({last_word})[1] + + -- if there is white space following the words + -- and we have at least one word following the last delimiter + -- then in all cases we are looking for ANY arg + if unicode.sub(line, -unicode.len(normal)) ~= normal then + return line, nil, "" + end + + local prefix = unicode.sub(line, 1, -unicode.len(normal) - 1) + + -- renormlizing the string will create 'printed' quality text + normal = text.internal.normalize(text.internal.tokenize(normal), true)[1] + + -- one word: cmd + -- many: arg + if last_close == num_splits - 1 then + return prefix, normal, nil + else + return prefix, nil, normal end - local l = text.internal.normalize({splits[num_splits]})[1] - return line:sub(1,-unicode.len(l)-1), l end --[[@delayloaded-end@]] function --[[@delayloaded-start@]] sh.internal.hintHandlerImpl(full_line, cursor) @@ -557,52 +609,47 @@ function --[[@delayloaded-start@]] sh.internal.hintHandlerImpl(full_line, cursor local line = unicode.sub(full_line, 1, cursor - 1) -- suffix: text following the cursor (if any, else empty string) to append to the hints local suffix = unicode.sub(full_line, cursor) - -- if there is no text to hint, there are no hints - if not line or #line < 1 then - return {} - end + -- hintHandlerSplit helps make the hints work even after delimiters such as ; -- it also catches parse errors such as unclosed quotes - local prev,line = sh.internal.hintHandlerSplit(line) - if not prev then -- failed to parse, e.g. unclosed quote, no hints + -- prev: not needed for this hint + -- cmd: the command needing hint + -- arg: the argument needing hint + local prev, cmd, arg = sh.internal.hintHandlerSplit(line) + + -- also, if there is no text to hint, there are no hints + if not prev then -- no hints e.g. unclosed quote, e.g. no text return {} end local result - -- prefix: text (if any) that will not be expanded (such as a command word preceding a file name that we are expanding) - -- partial: text that we want to expand - -- this first match determines if partial comes after redirect symbols such as > - local prefix, partial = line:match("^(.*[=><]%s*)(.*)$") - -- if redirection was not found, partial could just be arguments following a command - if not prefix then prefix, partial = line:match("^(.+%s+)(.*)$") end - -- partialPrefix: text of the partial that will not be expanded (i.e. a diretory path ending with /) - -- first, partialPrefix holds the whole text being expanded (we truncate later) - local partialPrefix = (partial or line) - -- name: text of the partial file name being expanded - local name = partialPrefix:gsub("^.*/", "") - -- here we remove the name text from the partialPrefix - partialPrefix = partialPrefix:sub(1, -unicode.len(name) - 1) - -- if no prefix was found and partialPrefix did not specify a closed directory path then we are expanding the first argument - -- i.e. the command word (a program name) - local searchInPath = not prefix and not partialPrefix:find("/") + + local searchInPath = cmd and not cmd:find("/") if searchInPath then - result = sh.getMatchingPrograms(line) + result = sh.getMatchingPrograms(cmd) else - result = sh.getMatchingFiles(partialPrefix, name) + -- special arg issue, after equal sign + if arg then + local equal_index = arg:find("=[^=]*$") + if equal_index then + prev = prev .. unicode.sub(arg, 1, equal_index) + arg = unicode.sub(arg, equal_index + 1) + end + end + result = sh.getMatchingFiles(cmd or arg) end + -- in very special cases, the suffix should include a blank space to indicate to the user that the hint is discrete local resultSuffix = suffix if #result > 0 and unicode.sub(result[1], -1) ~= "/" and not suffix:sub(1,1):find('%s') and - (#result == 1 or searchInPath or not prefix) then + (#result == 1 or cmd) then resultSuffix = " " .. resultSuffix end - -- prefix no longer needs to refer to just the expanding section of the text - -- here we reintroduce the previous section of the text that hintHandlerSplit cut for us - prefix = prev .. (prefix or "") + table.sort(result) for i = 1, #result do -- the hints define the whole line of text - result[i] = prefix .. result[i] .. resultSuffix + result[i] = prev .. result[i] .. resultSuffix end return result end --[[@delayloaded-end@]] @@ -796,7 +843,7 @@ function --[[@delayloaded-start@]] sh.internal.newMemoryStream() table.remove(self.result, 1) return self end - return nil, 'stream closed' + os.exit(0) -- abort the current process: SIGPIPE end local stream = {closed = false, buffer = "", diff --git a/src/main/resources/assets/opencomputers/loot/openos/lib/shell.lua b/src/main/resources/assets/opencomputers/loot/openos/lib/shell.lua index 9813f767b..5db0b4847 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/lib/shell.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/lib/shell.lua @@ -127,7 +127,7 @@ function shell.resolveAlias(command, args) end function shell.getWorkingDirectory() - return os.getenv("PWD") + return os.getenv("PWD") or "/" end function shell.setWorkingDirectory(dir) diff --git a/src/main/resources/assets/opencomputers/loot/openos/lib/term.lua b/src/main/resources/assets/opencomputers/loot/openos/lib/term.lua index d2a02ef32..2bef57a06 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/lib/term.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/lib/term.lua @@ -89,7 +89,7 @@ function term.internal.pull(input, c, off, t, ...) if gpu then gpu.set(x,y,c[3]) end - return select(2,unpack(a)) + return unpack(a) end local blinking = w.blink if input then blinking = input.blink end @@ -99,7 +99,12 @@ end function term.pull(p,...) local a,t = {p,...} if type(p) == "number" then t = table.remove(a,1) end - return term.internal.pull(nil,nil,nil,t,table.unpack(a)) + -- term.internal.pull captures hard interrupts to keep term.readKeyboard peaceful + -- but other scripts may be using term.pull as an event.pull replacement + -- in which case, term.pull need to abort on hard interrupt + local packed = {term.internal.pull(nil,nil,nil,t,table.unpack(a))} + assert(packed[1], "interrupted") + return select(2, table.unpack(packed)) end function term.read(history,dobreak,hintHandler,pwchar,filter) @@ -229,7 +234,7 @@ function term.readKeyboard(ops) term.internal.build_vertical_reader(input) end while true do - local name, address, char, code = term.internal.pull(input) + local killed, name, address, char, code = term.internal.pull(input) local c = nil local backup_cache = hints.cache if name =="interrupted" then draw("^C\n",true) return "" @@ -251,6 +256,8 @@ function term.readKeyboard(ops) input:move(ctrl and term.internal.ctrl_movement(input, -1) or -1) elseif code==keys.right then input:move(ctrl and term.internal.ctrl_movement(input, 1) or 1) + elseif ctrl and char=="w" then + -- cut word elseif code==keys.up then term.internal.read_history(history,input,1) elseif code==keys.down then @@ -383,6 +390,38 @@ function term.bind(gpu, screen, kb, window) end end +function --[[@delayloaded-start@]] term.scroll(number, window) + -- if zero scroll length is requested, do nothing + if number == 0 then return end + -- window is optional, default to current active terminal + window = window or W() + -- gpu works with global coordinates + local gpu,width,height,dx,dy,x,y = window.gpu,term.getViewport(w) + + -- scroll request can be too large + local abs_number = math.abs(number) + if (abs_number >= height) then + term.clear() + return + end + + -- box positions to shift + local box_height = height - abs_number + local top = 0 + if number > 0 then + top = number -- (e.g. 1 scroll moves box at 2) + end + + gpu.copy(dx + 1, dy + top + 1, width, box_height, 0, -number) + + local fill_top = 0 + if number > 0 then + fill_top = box_height + end + + gpu.fill(dx + 1, dy + fill_top + 1, width, abs_number, " ") +end --[[@delayloaded-end@]] + function --[[@delayloaded-start@]] term.internal.ctrl_movement(input, dir) local index, data = input.index, input.data diff --git a/src/main/resources/assets/opencomputers/loot/openos/lib/text.lua b/src/main/resources/assets/opencomputers/loot/openos/lib/text.lua index 9afc1c32b..3556923b6 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/lib/text.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/lib/text.lua @@ -80,24 +80,21 @@ end ------------------------------------------------------------------------------- --- tokenize allows nil for delimiters, quotes, and doNotNormalize --- always separates by whitespace --- default quote rules: '' and "" --- default delimiters: all --- default is to normalize, that is, no metadata is returned, just a list of tokens -function text.tokenize(value, doNotNormalize, quotes, delimiters) +-- separate string value into an array of words delimited by whitespace +-- groups by quotes +-- options is a table used for internal undocumented purposes +function text.tokenize(value, options) checkArg(1, value, "string") - checkArg(2, doNotNormalize, "boolean", "nil") - checkArg(3, quotes, "table", "nil") - checkArg(4, delimiters, "table", "nil") + checkArg(2, options, "table", "nil") + options = options or {} - local tokens, reason = text.internal.tokenize(value, quotes, delimiters) + local tokens, reason = text.internal.tokenize(value, options) if type(tokens) ~= "table" then return nil, reason end - if doNotNormalize then + if options.doNotNormalize then return tokens end @@ -113,6 +110,9 @@ function text.removeEscapes(txt) end ------------------------------------------------------------------------------- +-- like tokenize, but does not drop any text such as whitespace +-- splits input into an array for sub strings delimited by delimiters +-- delimiters are included in the result if not dropDelims function --[[@delayloaded-start@]] text.split(input, delimiters, dropDelims, di) checkArg(1, input, "string") checkArg(2, delimiters, "table") @@ -153,14 +153,15 @@ end --[[@delayloaded-end@]] ----------------------------------------------------------------------------- -function text.internal.tokenize(value, quotes, delimiters) +function text.internal.tokenize(value, options) checkArg(1, value, "string") - checkArg(2, quotes, "table", "nil") - checkArg(3, delimiters, "table", "nil") - local custom = not not delimiters + checkArg(2, options, "table", "nil") + options = options or {} + local delimiters = options.delimiters + local custom = not not options.delimiters delimiters = delimiters or text.syntax - local words, reason = text.internal.words(value, quotes) + local words, reason = text.internal.words(value, options) local splitter = text.escapeMagic(custom and table.concat(delimiters) or "<>|;&") if type(words) ~= "table" or @@ -173,9 +174,12 @@ function text.internal.tokenize(value, quotes, delimiters) end -- tokenize input by quotes and whitespace -function text.internal.words(input, quotes) +function text.internal.words(input, options) checkArg(1, input, "string") - checkArg(2, quotes, "table", "nil") + checkArg(2, options, "table", "nil") + options = options or {} + local quotes = options.quotes + local show_escapes = options.show_escapes local qr = nil quotes = quotes or {{"'","'",true},{'"','"'},{'`','`'}} local function append(dst, txt, qr) @@ -193,11 +197,12 @@ function text.internal.words(input, quotes) local char = unicode.sub(input, i, i) if escaped then -- escaped character escaped = false - -- include escape char if + -- include escape char if show_escapes + -- or the followwing are all true -- 1. qr active -- 2. the char escaped is NOT the qr closure -- 3. qr is not literal - if qr and not qr[3] and qr[2] ~= char then + if show_escapes or (qr and not qr[3] and qr[2] ~= char) then append(token, '\\', qr) end append(token, char, qr) diff --git a/src/main/resources/assets/opencomputers/loot/openos/lib/transforms.lua b/src/main/resources/assets/opencomputers/loot/openos/lib/transforms.lua index 18dfcf977..97a53bd44 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/lib/transforms.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/lib/transforms.lua @@ -54,16 +54,16 @@ function lib.first(tbl,pred,f,l) end -- if value was made by lib.sub then find can find from whence -function --[[@delayloaded-start@]] lib.find(tbl,v,f,l) - checkArg(1,tbl,'table') - checkArg(2,v,'table') - local s=#v - return lib.first(tbl,function(e,i,tbl) - for n=0,s-1 do - if tbl[n+i]~=v[n+1] then return nil end +function --[[@delayloaded-start@]] lib.find(tbl, sub, first, last) + checkArg(1, tbl, 'table') + checkArg(2, sub, 'table') + local sub_len = #sub + return lib.first(tbl, function(element, index, projected_table) + for n=0,sub_len-1 do + if projected_table[n + index] ~= sub[n + 1] then return nil end end - return 1,s - end,f,l) + return 1, sub_len + end, first, last) end --[[@delayloaded-end@]] -- Returns a list of subsets of tbl where partitioner acts as a delimiter. @@ -150,12 +150,14 @@ function lib.foreach(tbl,c,f,l) return r end lib.select=lib.foreach + function --[[@delayloaded-start@]] lib.where(tbl,p,f,l) return lib.foreach(tbl, function(e,i,tbl) return p(e,i,tbl)and e or nil end,f,l) end --[[@delayloaded-end@]] + function lib.concat(...) local r,rn,k={},0 for _,tbl in ipairs({...})do @@ -172,4 +174,19 @@ function lib.concat(...) return r end +-- works with pairs on tables +-- returns the kv pair, or nil and the number of pairs iterated +function --[[@delayloaded-start@]] lib.at(tbl, index) + checkArg(1, tbl, "table") + checkArg(2, index, "number", "nil") + local current_index = 1 + for k,v in pairs(tbl) do + if current_index == index then + return k,v + end + current_index = current_index + 1 + end + return nil, current_index - 1 -- went one too far +end --[[@delayloaded-end@]] + return lib,{adjust=adjust,view=view} diff --git a/src/main/resources/assets/opencomputers/loot/openos/usr/man/head b/src/main/resources/assets/opencomputers/loot/openos/usr/man/head index 1c89f4e66..98bed61da 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/usr/man/head +++ b/src/main/resources/assets/opencomputers/loot/openos/usr/man/head @@ -1,13 +1,13 @@ -NAME - head - Print the first 10 lines of each FILE to stdout. - -SYNOPSIS - head [OPTION]... [FILE]... - -DESCRIPTION +NAME + head - Print the first 10 lines of each FILE to stdout. + +SYNOPSIS + head [OPTION]... [FILE]... + +DESCRIPTION Print the first 10 lines of each FILE to stdout. With no FILE, or when FILE is -, read stdin. - + --bytes=[-]n print the first n bytes of each file' with the leading '-', print all but the last n bytes of each file @@ -18,14 +18,14 @@ DESCRIPTION -v, --verbose always print headers giving file names --help print help message -EXAMPLES - head - head - - Read next 10 lines from standard in and print to standard out, then close. - - head file.txt - Print first 10 lines of file.txt and print to stdout - - head -n 32 file.txt - Print first 32 lines of file.txt and print to stdout - +EXAMPLES + head + head - + Read next 10 lines from standard in and print to standard out, then close. + + head file.txt + Print first 10 lines of file.txt and print to stdout + + head -n 32 file.txt + Print first 32 lines of file.txt and print to stdout + diff --git a/src/main/resources/assets/opencomputers/loot/openos/usr/man/install b/src/main/resources/assets/opencomputers/loot/openos/usr/man/install new file mode 100644 index 000000000..6c669aaa6 --- /dev/null +++ b/src/main/resources/assets/opencomputers/loot/openos/usr/man/install @@ -0,0 +1,81 @@ +NAME + install - installs files from a source filesystem to a target filesystem + +SYNOPSIS + install [OPTIONS]... + +DESCRIPTION + install builds a list of candidate source and target mounted filesystems. If there are multiple candidates, the user is prompted for selections. By default, install copies all files from the source filesystem's root to the target filesystem's root path. If the source filesystem contains an .osprop, and unless command line options specify otherwise, the target filesystem's label is set and the user is prompted for reboot when install is complete. Alternatively, If a .lootprop file exists in the source filesystem, all default behavior is superceded and .lootprop is run as a script. .lootprop may copy files, set labels, prompt for reboot, etc. on its own. Developers creating their own .lootprop files for devices should respect environment variables set by install as per options it is given, such as ROOT. This manual page details the environment variables set by install when calling .lootprop. + +OPTIONS + --from=ADDR + When searching for candidate source filesystems, if specified, only mounted filesystem device addresses or their mount point paths that match ADDR will be included. By default, all filesystems except the rootfs are valid sources for install. If the user is trying to install rootfs to another filesystem, --from=ADDR is required where ADDR matches the rootfs device address or --from=/ + + --to=ADDR + same as --from but used when selecting target filesystem candidates. Note that the tmpfs is not a valid target filesystem by default, but must be specified explicitly if needed: i.e. --to=ADDR where ADDR matches the tmpfs device address or its mount point path, e.g. --to=/tmp . Note that install allows TO to equal FROM, also note that /bin/cp does not. But this detail may be noteworthy for .lootprop + + --fromDir=PATH + Install PATH from source. PATH is relative to the root of the source filesystem. The default is . + + --root=PATH + Same as --fromDir but for the target filesystem. + + --toDir=PATH + Same as --root. Either can be used. It is meaningless to specify both and is not documented which takes precedence in such a case. + + -u, --update + Indicates that install should prompt the user before modifying files. This invokes -i and -u for /bin/cp. + +The following only pertain when .osprop exists in the source filesystem. All environment variables are set for .lootprop regardless of the presense of .osprop + + --nolabelset + do not set target label + + --name=NAME + use NAME for label instead of any value specified by .osprop. This option is ignored if there is no .osprop (in which case, no label is set at all) + + --noboot + do not set target as default boot device when rebooting + + --noreboot + do not reboot after install + +.lootprop ENVIRONMENT + When .lootprop is loaded and executed, a custom table is added to the environment: ENV_.lootprop + These are the keys of the table as populated by install + + ENV_.lootprop.from: + This is the path of the selected source filesystem to install from. It should be the path to the executing .lootprop + example: /mnt/ABC + + ENV_.lootprop.to: + This is the path of the selected target filesystem to install to. + example: / + + _ENV.lootprop.fromdir + This is the relative path to use in the source filesysterm as passed by command line to install. If unspecified to install it defaults to "." + example: . + + _ENV.lootprop.root + This is the relative path to use in the target filesystem as passed by command line to install. If unspecified to install it defaults to "." + example: . + + _ENV.lootprop.update + Assigned value of --update, see OPTIONS + + _ENV.lootprop.nolabelset + Assigned value of --nolabelset, see OPTIONS + + _ENV.lootprop.name + Assigned value of --name, see OPTIONS + + _ENV.lootprop.noboot + Assigned value of --noboot, see OPTIONS + + --noreboot + Assigned value of --noreboot, see OPTIONS + +EXAMPLES + install + Searches all non rootfs filesystems to install from, and all non tmpfs filesystems to install to. Prompts the user for a selection, and copies. If .osprop is defined in source, sets label and will prompt for reboot when completed. + From 80e25f7be77131c7c340cd60ebade743b2aa704e Mon Sep 17 00:00:00 2001 From: payonel Date: Fri, 10 Jun 2016 22:52:00 -0700 Subject: [PATCH 4/7] 1.6 rc version 2 advanced install features as well as simplified install for common usage renamed .osprop to .prop support for named installed cp link copy fixes fix /bin/less pgdown and space package delay lookup no longer weak value fixed regression in sh tab complete for first arg completion --- .../assets/opencomputers/loot/openos/.osprop | 1 - .../assets/opencomputers/loot/openos/.prop | 1 + .../opencomputers/loot/openos/bin/cp.lua | 60 +-- .../opencomputers/loot/openos/bin/install.lua | 369 +++++++++++------- .../opencomputers/loot/openos/bin/less.lua | 2 +- .../opencomputers/loot/openos/lib/package.lua | 5 +- .../opencomputers/loot/openos/lib/sh.lua | 2 +- .../opencomputers/loot/openos/usr/man/install | 80 ++-- 8 files changed, 318 insertions(+), 202 deletions(-) delete mode 100644 src/main/resources/assets/opencomputers/loot/openos/.osprop create mode 100644 src/main/resources/assets/opencomputers/loot/openos/.prop diff --git a/src/main/resources/assets/opencomputers/loot/openos/.osprop b/src/main/resources/assets/opencomputers/loot/openos/.osprop deleted file mode 100644 index b2dc1ceff..000000000 --- a/src/main/resources/assets/opencomputers/loot/openos/.osprop +++ /dev/null @@ -1 +0,0 @@ -return {name = "OpenOS"} diff --git a/src/main/resources/assets/opencomputers/loot/openos/.prop b/src/main/resources/assets/opencomputers/loot/openos/.prop new file mode 100644 index 000000000..3671d643d --- /dev/null +++ b/src/main/resources/assets/opencomputers/loot/openos/.prop @@ -0,0 +1 @@ +return {label = "OpenOS", reboot=true, setlabel=true, setboot=true} diff --git a/src/main/resources/assets/opencomputers/loot/openos/bin/cp.lua b/src/main/resources/assets/opencomputers/loot/openos/bin/cp.lua index f805d5ef9..e5e023f90 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/bin/cp.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/bin/cp.lua @@ -64,8 +64,15 @@ end local function recurse(fromPath, toPath, origin) local isLink, target = fs.isLink(fromPath) - if isLink and options.P then + local toIsLink, toLinkTarget = fs.isLink(toPath) + local same_path = fs.canonical(isLink and target or fromPath) == fs.canonical(toIsLink and toLinkTarget or toPath) + local toExists = fs.exists(toPath) + + if isLink and options.P and (not toExists or not same_path) then status(fromPath, toPath) + if toIsLink then + fs.remove(toPath) + end return fs.link(target, toPath) elseif fs.isDirectory(fromPath) then if not options.r then @@ -95,35 +102,34 @@ local function recurse(fromPath, toPath, origin) end return true elseif fs.exists(fromPath) then - if fs.exists(toPath) then - if fs.canonical(fromPath) == fs.canonical(toPath) then + if toExists then + if same_path then return nil, "`" .. fromPath .. "' and `" .. toPath .. "' are the same file" end - if fs.isDirectory(toPath) then - if options.i then - if not prompt("overwrite `" .. toPath .. "'?") then - return true - end - elseif options.n then - return true - else -- yes, even for -f - return nil, "cannot overwrite directory `" .. toPath .. "' with non-directory" - end - else - if options.u then - if areEqual(fromPath, toPath) then - return true - end - end - if options.i then - if not prompt("overwrite `" .. toPath .. "'?") then - return true - end - elseif options.n then - return true - end - -- else: default to overwriting + + if options.n then + return true end + + -- if target is link, we are updating the target + if toIsLink then + toPath = toLinkTarget + end + + if options.u and not fs.isDirectory(toPath) and areEqual(fromPath, toPath) then + return true + end + + if options.i then + if not prompt("overwrite `" .. toPath .. "'?") then + return true + end + end + + if fs.isDirectory(toPath) then + return nil, "cannot overwrite directory `" .. toPath .. "' with non-directory" + end + fs.remove(toPath) end status(fromPath, toPath) diff --git a/src/main/resources/assets/opencomputers/loot/openos/bin/install.lua b/src/main/resources/assets/opencomputers/loot/openos/bin/install.lua index 6d3b1f990..d8a00d6ab 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/bin/install.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/bin/install.lua @@ -7,12 +7,21 @@ local shell = require("shell") local tx = require("transforms") local text = require("text") -local args, options = shell.parse(...) +local lib = {} -local sources = {} -local targets = {} +lib.args, lib.options = shell.parse(...) -if options.help then +lib.sources = {} +lib.targets = {} + +lib.source_label = lib.args[1] + +lib.stdout = io.stdout +lib.stderr = io.stderr +lib.stdin = io.stdin +lib.exit = os.exit + +if lib.options.help then print([[Usage: install [OPTION]... --from=ADDR install filesystem at ADDR default: builds list of @@ -22,25 +31,57 @@ if options.help then --root=PATH same as --fromDir but target --toDir=PATH same as --root -u, --update update files interactively -The following only pertain when .osprop exists - --nolabelset do not label target - --name override label from .osprop - --noboot do not use target for boot + --label override label from .prop + --nosetlabel do not label target + --nosetboot do not use target for boot --noreboot do not reboot after install]]) return nil -- exit success end local rootfs = fs.get("/") if not rootfs then - io.stderr:write("no root filesystem, aborting\n"); - return 1 + lib.stderr:write("no root filesystem, aborting\n"); + lib.exit(1) +end + +function lib.up_deprecate(old_key, new_key) + if lib.options[new_key] == nil then + lib.options[new_key] = lib.options[old_key] + end + lib.options[old_key] = nil +end + +function lib.cleanPath(path) + if path then + local rpath = shell.resolve(path) + if fs.isDirectory(rpath) then + return fs.canonical(rpath):gsub("/+$", "") .. '/' + end + end +end + +function lib.load_options() + lib.up_deprecate('noboot', 'nosetboot') + lib.up_deprecate('nolabelset', 'nosetlabel') + lib.up_deprecate('name', 'label') + + lib.source_root = lib.cleanPath(lib.options.from) + lib.target_root = lib.cleanPath(lib.options.to) + + lib.source_dir = (lib.options.fromDir or "") .. '/.' + lib.target_dir = (lib.options.root or lib.options.toDir or "") .. "/." + + lib.update = lib.options.u or lib.options.update + + lib.source_dev = lib.source_root and fs.get(lib.source_root) + lib.target_dev = lib.target_root and fs.get(lib.target_root) end local rootAddress = rootfs.address -- if the rootfs is read only, it is probably the loot disk! -local rootException = rootAddress +lib.rootException = rootAddress if rootfs.isReadOnly() then - rootException = nil + lib.rootException = nil end -- this may be OpenOS specific, default to "" in case no /dev mount point @@ -49,72 +90,96 @@ local devfsAddress = (fs.get("/dev/") or {}).address or "" -- tmp is only valid if specified as an option local tmpAddress = computer.tmpAddress() -local fromAddress = options.from -local toAddress = options.to -local fromDir = (options.fromDir or "") .. '/.' -local root = (options.root or options.toDir or "") .. "/." -options.update = options.u or options.update - -local function cleanPath(path) - if path then - local rpath = shell.resolve(path) - if fs.isDirectory(rpath) then - return fs.canonical(rpath):gsub("/+$", "") .. '/' +function lib.load(path, env) + if fs.exists(path) then + local loader, reason = loadfile(path, "bt", setmetatable(env or {}, {__index=_G})) + if not loader then + return nil, reason end + local ok, loaded = pcall(loader) + return ok and loaded, ok or loaded end - return path end -fromAddress = cleanPath(fromAddress) -toAddress = cleanPath(toAddress) - -local function validDevice(candidate, exceptions, specified, existing) +function lib.validDevice(candidate, exceptions, specified, existing) local address = candidate.dev.address if tx.first(existing, function(e) return e.dev.address == address end) then return end - local path = candidate.path if specified then - return address:find(specified, 1, true) == 1 or specified == path + if type(specified) == "string" and address:find(specified, 1, true) == 1 or specified == candidate.dev then + return true + end else return not tx.find(exceptions, {address}) end end +function lib.relevant(candidate, path) + if not path or fs.get(path) ~= candidate.dev then + return candidate.path + end + return path +end + -- use a single for loop of all filesystems to build the list of candidates of sources and targets -for dev, path in fs.mounts() do - local candidate = {dev=dev, path=path} +function lib.load_candidates() + for dev, path in fs.mounts() do + local candidate = {dev=dev, path=path:gsub("/+$","")..'/'} - if validDevice(candidate, {devfsAddress, tmpAddress, rootException}, fromAddress, sources) then - if fromAddress or fs.list(path)() then - table.insert(sources, candidate) + if lib.validDevice(candidate, {devfsAddress, tmpAddress, lib.rootException}, lib.source_dev or lib.options.from, lib.sources) then + local root_path = lib.relevant(candidate, lib.source_root) + if (lib.options.from or fs.list(root_path)()) then -- ignore empty sources unless specified + candidate.prop = lib.load(root_path .. "/.prop") or {} + if not lib.source_label or lib.source_label:lower() == (candidate.prop.label or candidate.dev.getLabel()):lower() then + table.insert(lib.sources, candidate) + end + end + end + + -- in case candidate is valid for BOTH, we want a new table + candidate = {dev=candidate.dev, path=candidate.path} -- but not the prop + + if lib.validDevice(candidate, {devfsAddress, tmpAddress}, lib.target_dev or lib.options.to, lib.targets) then + if not dev.isReadOnly() then + table.insert(lib.targets, candidate) + elseif lib.options.to then + lib.stderr:write("Cannot install to " .. lib.options.to .. ", it is read only\n") + lib.exit(1) + return false -- in lib mode this can be hit + end end end - if validDevice(candidate, {devfsAddress, tmpAddress}, toAddress, targets) then - if not dev.isReadOnly() then - table.insert(targets, candidate) - elseif toAddress then - io.stderr:write("Cannot install to " .. toAddress .. ", it is read only\n") - return 1 + return true +end + +function lib.check_sources() + if #lib.sources == 0 then + if lib.source_label then + lib.stderr:write("No filesystem to matched given label: " .. lib.source_label .. '\n') + elseif lib.options.from then + lib.stderr:write("No such filesystem to install from: " .. lib.options.from .. '\n') + else + lib.stderr:write("Could not find and available installations\n") end + lib.exit(1) end + return true end -if fromAddress and #sources == 0 then - io.stderr:write("No such filesystem to install from: " .. fromAddress .. "\n") - return 1 -end - -if #targets == 0 then - if toAddress then - io.stderr:write("No such filesystem to install to: " .. toAddress .. "\n") - else - io.stderr:write("No writable disks found, aborting\n") +function lib.check_targets() + if #lib.targets == 0 then + if lib.options.to then + lib.stderr:write("No such filesystem to install to: " .. lib.options.to .. '\n') + else + lib.stderr:write("No writable disks found, aborting\n") + end + lib.exit(1) end - return 1 + return true end ----- For now, I am allowing source==target -- cp can handle it if the user prepares conditions correctly @@ -124,11 +189,12 @@ end -- return 1 --end -local function prompt_select(devs, direction) +function lib.prompt_select(devs, direction) + table.sort(devs, function(a, b) return a.path 1 then - print("Select the device to install " .. direction) + lib.stdout:write("Select the device to install " .. direction .. '\n') for i = 1, #devs do local src = devs[i] @@ -138,118 +204,141 @@ local function prompt_select(devs, direction) else label = src.dev.address end - print(i .. ") " .. label .. " at " .. src.path) + lib.stdout:write(i .. ") " .. label .. " at " .. src.path .. '\n') end - print("Please enter a number between 1 and " .. #devs) - io.write("Enter 'q' to cancel the installation: ") + lib.stdout:write("Please enter a number between 1 and " .. #devs .. '\n') + lib.stdout:write("Enter 'q' to cancel the installation: ") local choice while not choice do - result = io.read() + result = lib.stdin:read() if result:sub(1, 1):lower() == "q" then - os.exit() + lib.exit() + return false end local number = tonumber(result) if number and number > 0 and number <= #devs then choice = devs[number] else - io.write("Invalid input, please try again: ") + lib.stdout:write("Invalid input, please try again: ") end end end - choice.display = (choice.path == '/' and "the root filesystem") or choice.dev.getLabel() or choice.path - - if #devs == 1 then - print("Selecting " .. choice.display .. " (only option)") - end + -- normally it is helpful to call / the root filesystem + -- but if rootfs is readonly, then we know we are using rootfs as a source + -- in which case, it's label takes priority + choice.display = + not choice.dev.isReadOnly() and (choice.path == '/' and "the root filesystem") or + -- everything has props by this point, except for targets + (choice.prop or {}).label or + choice.dev.getLabel() or + choice.path return choice end -table.sort(sources, function(a, b) return a.path 0 and unicode.sub(result[1], -1) ~= "/" and not suffix:sub(1,1):find('%s') and - (#result == 1 or cmd) then + #result == 1 or searchInPath then resultSuffix = " " .. resultSuffix end diff --git a/src/main/resources/assets/opencomputers/loot/openos/usr/man/install b/src/main/resources/assets/opencomputers/loot/openos/usr/man/install index 6c669aaa6..45c3ab25a 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/usr/man/install +++ b/src/main/resources/assets/opencomputers/loot/openos/usr/man/install @@ -2,20 +2,20 @@ NAME install - installs files from a source filesystem to a target filesystem SYNOPSIS - install [OPTIONS]... + install [name] [OPTIONS]... DESCRIPTION - install builds a list of candidate source and target mounted filesystems. If there are multiple candidates, the user is prompted for selections. By default, install copies all files from the source filesystem's root to the target filesystem's root path. If the source filesystem contains an .osprop, and unless command line options specify otherwise, the target filesystem's label is set and the user is prompted for reboot when install is complete. Alternatively, If a .lootprop file exists in the source filesystem, all default behavior is superceded and .lootprop is run as a script. .lootprop may copy files, set labels, prompt for reboot, etc. on its own. Developers creating their own .lootprop files for devices should respect environment variables set by install as per options it is given, such as ROOT. This manual page details the environment variables set by install when calling .lootprop. + install builds a list of candidate source and target mounted filesystems. If there are multiple candidates, the user is prompted for selections. By default, install copies all files from the source filesystem's root to the target filesystem's root path. The source filesystem can define label, boot, and reboot behavior via .prop and a fully custom install experience via .install which supercedes install running cp from source to target filesystems. Developers creating their own .install files for devices should respect environment variables set by install as per options it is given, such as the root path. This manual page details those environment variables. OPTIONS --from=ADDR - When searching for candidate source filesystems, if specified, only mounted filesystem device addresses or their mount point paths that match ADDR will be included. By default, all filesystems except the rootfs are valid sources for install. If the user is trying to install rootfs to another filesystem, --from=ADDR is required where ADDR matches the rootfs device address or --from=/ + Specifies the source filesystem or its root path. ADDR can be the device guid or a directory path. If this is a directory path, it represents a root path to install from. This option can also be used to specify source paths that would otherwise be ignored, those being devfs, tmpfs, and the rootfs. e.g. --from=/tmp . Note that if both --from and a [name] is given, install expects the source path to have a .prop defining the same name. See .prop for more details. --to=ADDR - same as --from but used when selecting target filesystem candidates. Note that the tmpfs is not a valid target filesystem by default, but must be specified explicitly if needed: i.e. --to=ADDR where ADDR matches the tmpfs device address or its mount point path, e.g. --to=/tmp . Note that install allows TO to equal FROM, also note that /bin/cp does not. But this detail may be noteworthy for .lootprop + Same as --from but specifies the target filesystem by guid or its root path. This option can also be used to specify filesystems that are otherwise ignored including tmpfs. i.e. --to=ADDR where ADDR matches the tmpfs device address or its mount point path. e.g. --to=/tmp --fromDir=PATH - Install PATH from source. PATH is relative to the root of the source filesystem. The default is . + Install PATH from source. PATH is relative to the root of the source filesystem or path given by --from. The default is . --root=PATH Same as --fromDir but for the target filesystem. @@ -26,56 +26,78 @@ OPTIONS -u, --update Indicates that install should prompt the user before modifying files. This invokes -i and -u for /bin/cp. -The following only pertain when .osprop exists in the source filesystem. All environment variables are set for .lootprop regardless of the presense of .osprop +The following can override settings defined in .prop in the source filesystem. - --nolabelset - do not set target label + --label=NAME + use NAME for label instead of any value specified by .prop, --name is deprecated - --name=NAME - use NAME for label instead of any value specified by .osprop. This option is ignored if there is no .osprop (in which case, no label is set at all) + --nosetlabel + do not set target label. --nolabelset is deprecated - --noboot - do not set target as default boot device when rebooting + --nosetboot + do not set target as default boot device when rebooting. --noboot is deprecated --noreboot do not reboot after install -.lootprop ENVIRONMENT - When .lootprop is loaded and executed, a custom table is added to the environment: ENV_.lootprop +.prop + .prop should have valid lua code that returns a table of keys and their values: e.g. "return {name='OpenOS'}" + + name=string + Declares an identifying name of the installation. This is displayed by install during source selection and also can be used on the commandline: e.g. (where {name="tape"} is given) `install tape`. If setlabel is true, this value is used for the target filesystem label. --name overrides this value. Note that install uses a case insensitive search: e.g. install TAPE works the same as install tape. + + setlabel=boolean + Determines whether the install should set the target filesystem's label. If .prop does not define a name key and the user does not define a command line --name=VALUE, setlabel has no action. --nosetlabel overrides this value + + setboot=boolean + Determines if the target filesystem should be set as the machine's default boot device. Default is false, overriden by --nosetboot + + reboot=boolean + Determines if the machine should reboot after the install completes. Overriden by --noreboot + + EXAMPLE: + return {name='OpenOS', setlabel=true, setboot=true, reboot=true} + +.install ENVIRONMENT + When .install is loaded and executed, a custom table is added to the environment: ENV_.install These are the keys of the table as populated by install - ENV_.lootprop.from: - This is the path of the selected source filesystem to install from. It should be the path to the executing .lootprop + ENV_.install.from: + This is the path of the selected source filesystem to install from. It should be the path to the executing .install example: /mnt/ABC - ENV_.lootprop.to: + ENV_.install.to: This is the path of the selected target filesystem to install to. example: / - _ENV.lootprop.fromdir + _ENV.install.fromdir This is the relative path to use in the source filesysterm as passed by command line to install. If unspecified to install it defaults to "." example: . - _ENV.lootprop.root + _ENV.install.root This is the relative path to use in the target filesystem as passed by command line to install. If unspecified to install it defaults to "." example: . - _ENV.lootprop.update + _ENV.install.update Assigned value of --update, see OPTIONS - _ENV.lootprop.nolabelset - Assigned value of --nolabelset, see OPTIONS + _ENV.install.label + Assigned value of --name or .prop's label, see OPTIONS - _ENV.lootprop.name - Assigned value of --name, see OPTIONS + _ENV.install.setlabel + Assigned value of .prop's setlabel unless --nolabelset, see OPTIONS - _ENV.lootprop.noboot - Assigned value of --noboot, see OPTIONS + _ENV.install.setboot + Assigned value of .prop's boot unless --nosetboot, see OPTIONS - --noreboot - Assigned value of --noreboot, see OPTIONS + _ENV.install.reboot + Assigned value of .prop's reboot unless --noreboot, see OPTIONS EXAMPLES install - Searches all non rootfs filesystems to install from, and all non tmpfs filesystems to install to. Prompts the user for a selection, and copies. If .osprop is defined in source, sets label and will prompt for reboot when completed. + Searches all non rootfs filesystems to install from, and all non tmpfs filesystems to install to. Prompts the user for a selection, and copies. If .prop is defined in source, sets label and will prompt for reboot when completed. + + install openos + Searches candidates source filesystems that have .prop's that define name="OpenOS" and prompts the user to confirm install to candidate target filesystems. + From 16dde2fc5bfedfdfdf80e6199a02a8ae6c54ed82 Mon Sep 17 00:00:00 2001 From: payonel Date: Sat, 11 Jun 2016 23:00:04 -0700 Subject: [PATCH 5/7] optimizations for install and cp and filesystem links fixes optmized code and split up install to reduce memory costs. Can now install on 1x T1 RAM systems. found old bug in filesystem: touch a ln a b mkdir d cd d touch b rm b And the link in the parent dir would be lost. The reason is that the virtual nodes created for the link were being selected when removing the b file in d, as well as removing b from the real fs. The fix was to check that the node was the correct node, and not just remove the key from any node returned from findNode --- .../assets/opencomputers/loot/openos/.prop | 2 +- .../opencomputers/loot/openos/bin/cp.lua | 19 +- .../opencomputers/loot/openos/bin/install.lua | 362 ++---------------- .../loot/openos/lib/filesystem.lua | 25 +- .../loot/openos/lib/tools/install_basics.lua | 207 ++++++++++ .../loot/openos/lib/tools/install_utils.lua | 84 ++++ 6 files changed, 354 insertions(+), 345 deletions(-) create mode 100644 src/main/resources/assets/opencomputers/loot/openos/lib/tools/install_basics.lua create mode 100644 src/main/resources/assets/opencomputers/loot/openos/lib/tools/install_utils.lua diff --git a/src/main/resources/assets/opencomputers/loot/openos/.prop b/src/main/resources/assets/opencomputers/loot/openos/.prop index 3671d643d..5cf0ed07b 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/.prop +++ b/src/main/resources/assets/opencomputers/loot/openos/.prop @@ -1 +1 @@ -return {label = "OpenOS", reboot=true, setlabel=true, setboot=true} +{label = "OpenOS", reboot=true, setlabel=true, setboot=true} diff --git a/src/main/resources/assets/opencomputers/loot/openos/bin/cp.lua b/src/main/resources/assets/opencomputers/loot/openos/bin/cp.lua index e5e023f90..6f2ba1d07 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/bin/cp.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/bin/cp.lua @@ -1,5 +1,6 @@ local fs = require("filesystem") local shell = require("shell") +local computer = require("computer") local args, options = shell.parse(...) if #args < 2 then @@ -18,11 +19,17 @@ end local exit_code = nil options.P = options.P or options.r +-- interrupting is important, but not EVERY copy +local greedy = computer.uptime() + local function status(from, to) if options.v then io.write(from .. " -> " .. to .. "\n") end - os.sleep(0) -- allow interrupting + if computer.uptime() - greedy > 4 then + os.sleep(0) -- allow interrupting + greedy = computer.uptime() + end end local result, reason @@ -69,10 +76,14 @@ local function recurse(fromPath, toPath, origin) local toExists = fs.exists(toPath) if isLink and options.P and (not toExists or not same_path) then - status(fromPath, toPath) - if toIsLink then - fs.remove(toPath) + if toExists and options.n then + return true end + fs.remove(toPath) + if toExists and options.v then + io.write(string.format("removed '%s'\n", toPath)) + end + status(fromPath, toPath) return fs.link(target, toPath) elseif fs.isDirectory(fromPath) then if not options.r then diff --git a/src/main/resources/assets/opencomputers/loot/openos/bin/install.lua b/src/main/resources/assets/opencomputers/loot/openos/bin/install.lua index d8a00d6ab..65029f329 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/bin/install.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/bin/install.lua @@ -1,344 +1,44 @@ -local component = require("component") local computer = require("computer") -local event = require("event") -local fs = require("filesystem") -local unicode = require("unicode") local shell = require("shell") -local tx = require("transforms") -local text = require("text") -local lib = {} +local options -lib.args, lib.options = shell.parse(...) - -lib.sources = {} -lib.targets = {} - -lib.source_label = lib.args[1] - -lib.stdout = io.stdout -lib.stderr = io.stderr -lib.stdin = io.stdin -lib.exit = os.exit - -if lib.options.help then - print([[Usage: install [OPTION]... - --from=ADDR install filesystem at ADDR - default: builds list of - candidates and prompts user - --to=ADDR same as --from but for target - --fromDir=PATH install PATH from source - --root=PATH same as --fromDir but target - --toDir=PATH same as --root - -u, --update update files interactively - --label override label from .prop - --nosetlabel do not label target - --nosetboot do not use target for boot - --noreboot do not reboot after install]]) - return nil -- exit success +do + local basic = loadfile("/lib/tools/install_basics.lua", "bt", _G) + options = basic(...) end -local rootfs = fs.get("/") -if not rootfs then - lib.stderr:write("no root filesystem, aborting\n"); - lib.exit(1) +if computer.freeMemory() < 50000 then + print("Low memory, collecting garbage") + for i=1,20 do os.sleep(0) end end -function lib.up_deprecate(old_key, new_key) - if lib.options[new_key] == nil then - lib.options[new_key] = lib.options[old_key] - end - lib.options[old_key] = nil +local cp, reason = loadfile(shell.resolve("cp", "lua"), "bt", _G) + +local ec = cp(table.unpack(options.cp_args)) +if ec ~= nil and ec ~= 0 then + return ec end -function lib.cleanPath(path) - if path then - local rpath = shell.resolve(path) - if fs.isDirectory(rpath) then - return fs.canonical(rpath):gsub("/+$", "") .. '/' - end +local write = io.write +local read = io.read +write("Installation complete!\n") + +if options.setlabel then + pcall(options.target.dev.setLabel, options.label) +end + +if options.setboot then + computer.setBootAddress(options.target.dev.address) +end + +if options.reboot then + write("Reboot now? [Y/n] ") + local result = read() + if not result or result == "" or result:sub(1, 1):lower() == "y" then + write("\nRebooting now!\n") + computer.shutdown(true) end end -function lib.load_options() - lib.up_deprecate('noboot', 'nosetboot') - lib.up_deprecate('nolabelset', 'nosetlabel') - lib.up_deprecate('name', 'label') - - lib.source_root = lib.cleanPath(lib.options.from) - lib.target_root = lib.cleanPath(lib.options.to) - - lib.source_dir = (lib.options.fromDir or "") .. '/.' - lib.target_dir = (lib.options.root or lib.options.toDir or "") .. "/." - - lib.update = lib.options.u or lib.options.update - - lib.source_dev = lib.source_root and fs.get(lib.source_root) - lib.target_dev = lib.target_root and fs.get(lib.target_root) -end - -local rootAddress = rootfs.address --- if the rootfs is read only, it is probably the loot disk! -lib.rootException = rootAddress -if rootfs.isReadOnly() then - lib.rootException = nil -end - --- this may be OpenOS specific, default to "" in case no /dev mount point -local devfsAddress = (fs.get("/dev/") or {}).address or "" - --- tmp is only valid if specified as an option -local tmpAddress = computer.tmpAddress() - -function lib.load(path, env) - if fs.exists(path) then - local loader, reason = loadfile(path, "bt", setmetatable(env or {}, {__index=_G})) - if not loader then - return nil, reason - end - local ok, loaded = pcall(loader) - return ok and loaded, ok or loaded - end -end - -function lib.validDevice(candidate, exceptions, specified, existing) - local address = candidate.dev.address - - if tx.first(existing, function(e) return e.dev.address == address end) then - return - end - - if specified then - if type(specified) == "string" and address:find(specified, 1, true) == 1 or specified == candidate.dev then - return true - end - else - return not tx.find(exceptions, {address}) - end -end - -function lib.relevant(candidate, path) - if not path or fs.get(path) ~= candidate.dev then - return candidate.path - end - return path -end - --- use a single for loop of all filesystems to build the list of candidates of sources and targets -function lib.load_candidates() - for dev, path in fs.mounts() do - local candidate = {dev=dev, path=path:gsub("/+$","")..'/'} - - if lib.validDevice(candidate, {devfsAddress, tmpAddress, lib.rootException}, lib.source_dev or lib.options.from, lib.sources) then - local root_path = lib.relevant(candidate, lib.source_root) - if (lib.options.from or fs.list(root_path)()) then -- ignore empty sources unless specified - candidate.prop = lib.load(root_path .. "/.prop") or {} - if not lib.source_label or lib.source_label:lower() == (candidate.prop.label or candidate.dev.getLabel()):lower() then - table.insert(lib.sources, candidate) - end - end - end - - -- in case candidate is valid for BOTH, we want a new table - candidate = {dev=candidate.dev, path=candidate.path} -- but not the prop - - if lib.validDevice(candidate, {devfsAddress, tmpAddress}, lib.target_dev or lib.options.to, lib.targets) then - if not dev.isReadOnly() then - table.insert(lib.targets, candidate) - elseif lib.options.to then - lib.stderr:write("Cannot install to " .. lib.options.to .. ", it is read only\n") - lib.exit(1) - return false -- in lib mode this can be hit - end - end - end - - return true -end - -function lib.check_sources() - if #lib.sources == 0 then - if lib.source_label then - lib.stderr:write("No filesystem to matched given label: " .. lib.source_label .. '\n') - elseif lib.options.from then - lib.stderr:write("No such filesystem to install from: " .. lib.options.from .. '\n') - else - lib.stderr:write("Could not find and available installations\n") - end - lib.exit(1) - end - return true -end - -function lib.check_targets() - if #lib.targets == 0 then - if lib.options.to then - lib.stderr:write("No such filesystem to install to: " .. lib.options.to .. '\n') - else - lib.stderr:write("No writable disks found, aborting\n") - end - lib.exit(1) - end - return true -end - ------ For now, I am allowing source==target -- cp can handle it if the user prepares conditions correctly ------ in other words, install doesn't need to filter this scenario: ---if #targets == 1 and #sources == 1 and targets[1] == sources[1] then --- io.stderr:write("It is not the intent of install to use the same source and target filesystem.\n") --- return 1 ---end - -function lib.prompt_select(devs, direction) - table.sort(devs, function(a, b) return a.path 1 then - lib.stdout:write("Select the device to install " .. direction .. '\n') - - for i = 1, #devs do - local src = devs[i] - local label = src.dev.getLabel() - if label then - label = label .. " (" .. src.dev.address:sub(1, 8) .. "...)" - else - label = src.dev.address - end - lib.stdout:write(i .. ") " .. label .. " at " .. src.path .. '\n') - end - - lib.stdout:write("Please enter a number between 1 and " .. #devs .. '\n') - lib.stdout:write("Enter 'q' to cancel the installation: ") - local choice - while not choice do - result = lib.stdin:read() - if result:sub(1, 1):lower() == "q" then - lib.exit() - return false - end - local number = tonumber(result) - if number and number > 0 and number <= #devs then - choice = devs[number] - else - lib.stdout:write("Invalid input, please try again: ") - end - end - end - - -- normally it is helpful to call / the root filesystem - -- but if rootfs is readonly, then we know we are using rootfs as a source - -- in which case, it's label takes priority - choice.display = - not choice.dev.isReadOnly() and (choice.path == '/' and "the root filesystem") or - -- everything has props by this point, except for targets - (choice.prop or {}).label or - choice.dev.getLabel() or - choice.path - - return choice -end - -function lib.load_env() - lib.env = - { - from=lib.source_root, - to=lib.target_root, - fromDir=lib.source_dir, - root=lib.target_dir, - update=lib.options.update, - label=lib.options.label or lib.source.prop.label, - setlabel=lib.source.prop.setlabel and not lib.options.nosetlabel, - setboot=lib.source.prop.setboot and not lib.options.nosetboot, - reboot=lib.source.prop.reboot and not lib.options.noreboot, - } -end - -function lib.init() - lib.load_options() - if not lib.load_candidates() then return false end - if not lib.check_sources() then return false end - if not lib.check_targets() then return false end - - lib.source = lib.prompt_select(lib.sources, "from") - if not lib.source then return false end - lib.source_root = lib.source_root or lib.source.path - - lib.target = lib.prompt_select(lib.targets, "to") - if not lib.target then return false end - lib.target_root = lib.target_root or lib.target.path - - lib.load_env() - local reason - lib.installer, reason = lib.load(lib.source_root .. '/.install', {install=lib.env}) - if not lib.installer then - if reason then - lib.stderr:write("installer failed to load: " .. tostring(reason) .. '\n') - lib.exit(1) - return false - else - lib.installer = lib.run - end - end - - return true -end - -function lib.run() - local cp = shell.resolve("cp", "lua") - local cp_options = "-vrx" .. (lib.options.update and "ui" or "") - local cp_source = (lib.source_root .. lib.source_dir):gsub("/+","/") - local cp_dest = (lib.target_root .. lib.target_dir):gsub("/+","/") - - io.write("Install " .. lib.source.display .. " to " .. lib.target.display .. "? [Y/n] ") - local choice = text.trim(lib.stdin:read()):lower() - if choice == "" then - choice = "y" - end - if choice ~= "y" then - lib.stdout:write("Installation cancelled\n") - lib.exit() - return false - end - - local message = string.format("Installing %s [%s] to %s [%s]", lib.source.display, cp_source, lib.target.display, cp_dest) - local cmd = cp .. ' ' .. cp_options .. ' ' .. cp_source .. ' ' .. cp_dest - lib.stdout:write(message .. '\n') - lib.stdout:write(cmd .. '\n') - os.sleep(0.25) - - local result, reason = os.execute(cmd) - - if not result then - error(reason, 0) - end - - lib.stdout:write("Installation complete!\n") - - if lib.env.setlabel then - pcall(lib.target.dev.setLabel, lib.env.label) - end - - local prereboot = function()end - if lib.env.setboot then - prereboot = computer.setBootAddress - end - - if lib.env.reboot then - lib.stdout:write("Reboot now? [Y/n] ") - local result = lib.stdin:read() - if not result or result == "" or result:sub(1, 1):lower() == "y" then - prereboot(lib.target.dev.address) - lib.stdout:write("\nRebooting now!\n") - computer.shutdown(true) - end - end - - lib.stdout:write("Returning to shell.\n") -end - -if lib.options.lib then - return lib -end - -lib.init() -lib.run() +write("Returning to shell.\n") diff --git a/src/main/resources/assets/opencomputers/loot/openos/lib/filesystem.lua b/src/main/resources/assets/opencomputers/loot/openos/lib/filesystem.lua index 5cd81b6a6..ea6896f67 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/lib/filesystem.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/lib/filesystem.lua @@ -378,16 +378,23 @@ end function filesystem.remove(path) local function removeVirtual() local node, rest, vnode, vrest = findNode(filesystem.path(path)) - local name = filesystem.name(path) - if vnode.children[name] then - vnode.children[name] = nil - removeEmptyNodes(vnode) - return true - elseif vnode.links[name] then - vnode.links[name] = nil - removeEmptyNodes(vnode) - return true + -- vrest represents the remaining path beyond vnode + -- vrest is nil if vnode reaches the full path + -- thus, if vrest is NOT NIL, then we SHOULD NOT remove children nor links + if not vrest then + local name = filesystem.name(path) + if vnode.children[name] then + vnode.children[name] = nil + removeEmptyNodes(vnode) + return true + elseif vnode.links[name] then + vnode.links[name] = nil + removeEmptyNodes(vnode) + return true + end end + -- return false even if vrest is nil because this means it was a expected + -- to be a real file return false end local function removePhysical() diff --git a/src/main/resources/assets/opencomputers/loot/openos/lib/tools/install_basics.lua b/src/main/resources/assets/opencomputers/loot/openos/lib/tools/install_basics.lua new file mode 100644 index 000000000..13848399f --- /dev/null +++ b/src/main/resources/assets/opencomputers/loot/openos/lib/tools/install_basics.lua @@ -0,0 +1,207 @@ +local computer = require("computer") +local shell = require("shell") +local component = require("component") +local event = require("event") +local fs = require("filesystem") +local unicode = require("unicode") +local text = require("text") + +local write = io.write +local read = io.read + +local args, options = shell.parse(...) + +options.sources = {} +options.targets = {} +options.source_label = args[1] + +local root_exception + +if options.help then + print([[Usage: install [OPTION]... + --from=ADDR install filesystem at ADDR + default: builds list of + candidates and prompts user + --to=ADDR same as --from but for target + --fromDir=PATH install PATH from source + --root=PATH same as --fromDir but target + --toDir=PATH same as --root + -u, --update update files interactively + --label override label from .prop + --nosetlabel do not label target + --nosetboot do not use target for boot + --noreboot do not reboot after install]]) + return nil -- exit success +end + +local rootfs = fs.get("/") +if not rootfs then + io.stderr:write("no root filesystem, aborting\n"); + os.exit(1) +end + +local function up_deprecate(old_key, new_key) + if options[new_key] == nil then + options[new_key] = options[old_key] + end + options[old_key] = nil +end + +local function cleanPath(path) + if path then + local rpath = shell.resolve(path) + if fs.isDirectory(rpath) then + return fs.canonical(rpath):gsub("/+$", "") .. '/' + end + end +end + +local rootAddress = rootfs.address +-- if the rootfs is read only, it is probably the loot disk! +root_exception = rootAddress +if rootfs.isReadOnly() then + root_exception = nil +end + +-- this may be OpenOS specific, default to "" in case no /dev mount point +local devfsAddress = (fs.get("/dev/") or {}).address or "" + +-- tmp is only valid if specified as an option +local tmpAddress = computer.tmpAddress() + +----- For now, I am allowing source==target -- cp can handle it if the user prepares conditions correctly +----- in other words, install doesn't need to filter this scenario: +--if #options.targets == 1 and #options.sources == 1 and options.targets[1] == options.sources[1] then +-- io.stderr:write("It is not the intent of install to use the same source and target filesystem.\n") +-- return 1 +--end + +------ load options +up_deprecate('noboot', 'nosetboot') +up_deprecate('nolabelset', 'nosetlabel') +up_deprecate('name', 'label') + +options.source_root = cleanPath(options.from) +options.target_root = cleanPath(options.to) + +options.source_dir = (options.fromDir or "") .. '.' +options.target_dir = (options.root or options.toDir or "") + +options.update = options.u or options.update + +local function path_to_dev(path) + return path and fs.isDirectory(path) and not fs.isLink(path) and fs.get(path) +end + +local source_dev = path_to_dev(options.source_root) +local target_dev = path_to_dev(options.target_root) + +-- use a single for loop of all filesystems to build the list of candidates of sources and targets +local function validDevice(candidate, exceptions, specified, existing) + local address = candidate.dev.address + + for _,e in ipairs(existing) do + if e.dev.address == address then + return + end + end + + if specified then + if address:find(specified, 1, true) == 1 then + return true + end + else + for _,e in ipairs(exceptions) do + if e == address then + return + end + end + return true + end +end + +for dev, path in fs.mounts() do + local candidate = {dev=dev, path=path:gsub("/+$","")..'/'} + + if validDevice(candidate, {devfsAddress, tmpAddress, root_exception}, source_dev and source_dev.address or options.from, options.sources) then + -- root path is either the command line path given for this dev or its natural mount point + local root_path = source_dev == dev and options.source_root or path + if (options.from or fs.list(root_path)()) then -- ignore empty sources unless specified + local prop = fs.open(root_path .. '/.prop') + if prop then + local prop_data = prop:read(math.huge) + prop:close() + prop = prop_data + end + candidate.prop = prop and load('return ' .. prop)() or {} + if not options.source_label or options.source_label:lower() == (candidate.prop.label or dev.getLabel()):lower() then + table.insert(options.sources, candidate) + end + end + end + + -- in case candidate is valid for BOTH, we want a new table + candidate = {dev=candidate.dev, path=candidate.path} -- but not the prop + + if validDevice(candidate, {devfsAddress, tmpAddress}, target_dev and target_dev.address or options.to, options.targets) then + if not dev.isReadOnly() then + table.insert(options.targets, candidate) + elseif options.to then + io.stderr:write("Cannot install to " .. options.to .. ", it is read only\n") + os.exit(1) + end + end +end + +local source = options.sources[1] +local target = options.targets[1] + +if #options.sources ~= 1 or #options.targets ~= 1 then + source, target = loadfile("/lib/tools/install_utils.lua", "bt", _G)('select', options) +end + +if not source then return end +options.source_root = options.source_root or source.path + +if not target then return end +options.target_root = options.target_root or target.path + +-- now that source is selected, we can update options +options.label = options.label or source.prop.label +options.setlabel = source.prop.setlabel and not options.nosetlabel +options.setboot = source.prop.setboot and not options.nosetboot +options.reboot = source.prop.reboot and not options.noreboot + +local installer_path = options.source_root .. "/.install" +if fs.exists(installer_path) then + return loadfile("/lib/tools/install_utils.lua", "bt", _G)('install', options) +end + +local cp_args = +{ + "-vrx" .. (options.update and "ui" or ""), + options.source_root .. options.source_dir, + options.target_root .. options.target_dir +} + +local source_display = (source.prop or {}).label or source.dev.getLabel() or source.path +local special_target = "" +if #options.targets > 1 or options.to then + special_target = " to " .. cp_args[3] +end +io.write("Install " .. source_display .. special_target .. "? [Y/n] ") +local choice = read():lower() +if choice ~= "y" and choice ~= "" then + write("Installation cancelled\n") + os.exit() +end + +return +{ + setlabel = options.setlabel, + label = options.label, + setboot = options.setboot, + reboot = options.reboot, + target = target, + cp_args = cp_args, +} diff --git a/src/main/resources/assets/opencomputers/loot/openos/lib/tools/install_utils.lua b/src/main/resources/assets/opencomputers/loot/openos/lib/tools/install_utils.lua new file mode 100644 index 000000000..fcce42170 --- /dev/null +++ b/src/main/resources/assets/opencomputers/loot/openos/lib/tools/install_utils.lua @@ -0,0 +1,84 @@ +local cmd, options = ... + +local function select_prompt(devs, direction) + table.sort(devs, function(a, b) return a.path 1 then + io.write("Select the device to install " .. direction .. '\n') + + for i = 1, #devs do + local src = devs[i] + local label = src.dev.getLabel() + if label then + label = label .. " (" .. src.dev.address:sub(1, 8) .. "...)" + else + label = src.dev.address + end + io.write(i .. ") " .. label .. " at " .. src.path .. '\n') + end + + io.write("Please enter a number between 1 and " .. #devs .. '\n') + io.write("Enter 'q' to cancel the installation: ") + choice = nil + while not choice do + result = io.read() + if result:sub(1, 1):lower() == "q" then + os.exit() + end + local number = tonumber(result) + if number and number > 0 and number <= #devs then + choice = devs[number] + else + io.write("Invalid input, please try again: ") + end + end + end + + return choice +end + +if cmd == 'select' then + if #options.sources == 0 then + if options.source_label then + io.stderr:write("No install source matched given label: " .. options.source_label .. '\n') + elseif options.from then + io.stderr:write("No install source found: " .. options.from .. '\n') + else + io.stderr:write("Could not find any available installations\n") + end + os.exit(1) + end + + if #options.targets == 0 then + if options.to then + io.stderr:write("No such filesystem to install to: " .. options.to .. '\n') + else + io.stderr:write("No writable disks found, aborting\n") + end + os.exit(1) + end + + return select_prompt(options.sources, "from"), select_prompt(options.targets, "to") + +elseif cmd == 'install' then + local installer_path = options.source_root .. "/.install" + local installer, reason = loadfile(installer_path, "bt", {install= + { + from=options.source_root, + to=options.target_root, + fromDir=options.source_dir, + root=options.target_dir, + update=options.update, + label=options.label, + setlabel=options.setlabel, + setboot=options.setboot, + reboot=options.reboot, + }}) + if not installer then + io.stderr:write("installer failed to load: " .. tostring(reason) .. '\n') + os.exit(1) + else + return installer() + end +end \ No newline at end of file From 698f71d753f0d4f5d3a88971bd07ba9c1442242f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Sun, 12 Jun 2016 20:27:28 +0200 Subject: [PATCH 6/7] Shuffled some of the old loot disks file structure to work well with new install. Removed (most) .autoruns on old loot disks. --- .../loot/builder/{ => bin}/build.lua | 0 .../loot/builder/{ => usr/man}/build.man | 0 .../{ => usr/share/build-plans}/example.plan | 0 .../opencomputers/loot/data/.autorun.lua | 31 ------------------- .../opencomputers/loot/dig/{ => bin}/dig.lua | 0 .../loot/dig/{ => usr/man}/dig.man | 0 .../opencomputers/loot/generator/.autorun.lua | 31 ------------------- .../opencomputers/loot/internet/.autorun.lua | 31 ------------------- .../opencomputers/loot/irc/{ => bin}/irc.lua | 0 .../loot/maze/{ => bin}/maze.lua | 0 .../loot/maze/{ => usr/man}/maze.man | 0 .../loot/openos/lib/tools/install_utils.lua | 4 +-- .../assets/opencomputers/loot/oppm/.install | 1 + 13 files changed, 3 insertions(+), 95 deletions(-) rename src/main/resources/assets/opencomputers/loot/builder/{ => bin}/build.lua (100%) rename src/main/resources/assets/opencomputers/loot/builder/{ => usr/man}/build.man (100%) rename src/main/resources/assets/opencomputers/loot/builder/{ => usr/share/build-plans}/example.plan (100%) delete mode 100644 src/main/resources/assets/opencomputers/loot/data/.autorun.lua rename src/main/resources/assets/opencomputers/loot/dig/{ => bin}/dig.lua (100%) rename src/main/resources/assets/opencomputers/loot/dig/{ => usr/man}/dig.man (100%) delete mode 100644 src/main/resources/assets/opencomputers/loot/generator/.autorun.lua delete mode 100644 src/main/resources/assets/opencomputers/loot/internet/.autorun.lua rename src/main/resources/assets/opencomputers/loot/irc/{ => bin}/irc.lua (100%) rename src/main/resources/assets/opencomputers/loot/maze/{ => bin}/maze.lua (100%) rename src/main/resources/assets/opencomputers/loot/maze/{ => usr/man}/maze.man (100%) create mode 100644 src/main/resources/assets/opencomputers/loot/oppm/.install diff --git a/src/main/resources/assets/opencomputers/loot/builder/build.lua b/src/main/resources/assets/opencomputers/loot/builder/bin/build.lua similarity index 100% rename from src/main/resources/assets/opencomputers/loot/builder/build.lua rename to src/main/resources/assets/opencomputers/loot/builder/bin/build.lua diff --git a/src/main/resources/assets/opencomputers/loot/builder/build.man b/src/main/resources/assets/opencomputers/loot/builder/usr/man/build.man similarity index 100% rename from src/main/resources/assets/opencomputers/loot/builder/build.man rename to src/main/resources/assets/opencomputers/loot/builder/usr/man/build.man diff --git a/src/main/resources/assets/opencomputers/loot/builder/example.plan b/src/main/resources/assets/opencomputers/loot/builder/usr/share/build-plans/example.plan similarity index 100% rename from src/main/resources/assets/opencomputers/loot/builder/example.plan rename to src/main/resources/assets/opencomputers/loot/builder/usr/share/build-plans/example.plan diff --git a/src/main/resources/assets/opencomputers/loot/data/.autorun.lua b/src/main/resources/assets/opencomputers/loot/data/.autorun.lua deleted file mode 100644 index f83416f02..000000000 --- a/src/main/resources/assets/opencomputers/loot/data/.autorun.lua +++ /dev/null @@ -1,31 +0,0 @@ -local event = require("event") -local fs = require("filesystem") -local process = require("process") - -local proxy = ... - --- Install symlinks if they don't already exist. -local links = {} -local fsroot = fs.path(process.running()) -local function inject(path) - for file in fs.list(fs.concat(fsroot, path)) do - local source = fs.concat(fsroot, path, file) - local target = fs.concat(path, file) - if fs.link(source, target) then - table.insert(links, target) - end - end -end -inject("lib") -inject("bin") -inject("usr/man") - --- Delete symlinks on removal. -event.listen("component_removed", function(_, address) - if address == proxy.address then - for _, link in ipairs(links) do - fs.remove(link) - end - return false -- remove listener - end -end) diff --git a/src/main/resources/assets/opencomputers/loot/dig/dig.lua b/src/main/resources/assets/opencomputers/loot/dig/bin/dig.lua similarity index 100% rename from src/main/resources/assets/opencomputers/loot/dig/dig.lua rename to src/main/resources/assets/opencomputers/loot/dig/bin/dig.lua diff --git a/src/main/resources/assets/opencomputers/loot/dig/dig.man b/src/main/resources/assets/opencomputers/loot/dig/usr/man/dig.man similarity index 100% rename from src/main/resources/assets/opencomputers/loot/dig/dig.man rename to src/main/resources/assets/opencomputers/loot/dig/usr/man/dig.man diff --git a/src/main/resources/assets/opencomputers/loot/generator/.autorun.lua b/src/main/resources/assets/opencomputers/loot/generator/.autorun.lua deleted file mode 100644 index 9aff88263..000000000 --- a/src/main/resources/assets/opencomputers/loot/generator/.autorun.lua +++ /dev/null @@ -1,31 +0,0 @@ -local event = require("event") -local fs = require("filesystem") -local process = require("process") - -local proxy = ... - --- Install symlinks if they don't already exist. -local links = {} -local fsroot = fs.path(process.running()) -local function inject(path) - for file in fs.list(fs.concat(fsroot, path)) do - local source = fs.concat(fsroot, path, file) - local target = fs.concat(path, file) - if fs.link(source, target) then - table.insert(links, target) - end - end -end -inject("lib") -inject("bin") -inject("usr/man") - --- Delete symlinks on removal. -event.listen("component_removed", function(_, address) - if address == proxy.address then - for _, link in ipairs(links) do - fs.remove(link) - end - return false -- remove listener - end -end) \ No newline at end of file diff --git a/src/main/resources/assets/opencomputers/loot/internet/.autorun.lua b/src/main/resources/assets/opencomputers/loot/internet/.autorun.lua deleted file mode 100644 index 9aff88263..000000000 --- a/src/main/resources/assets/opencomputers/loot/internet/.autorun.lua +++ /dev/null @@ -1,31 +0,0 @@ -local event = require("event") -local fs = require("filesystem") -local process = require("process") - -local proxy = ... - --- Install symlinks if they don't already exist. -local links = {} -local fsroot = fs.path(process.running()) -local function inject(path) - for file in fs.list(fs.concat(fsroot, path)) do - local source = fs.concat(fsroot, path, file) - local target = fs.concat(path, file) - if fs.link(source, target) then - table.insert(links, target) - end - end -end -inject("lib") -inject("bin") -inject("usr/man") - --- Delete symlinks on removal. -event.listen("component_removed", function(_, address) - if address == proxy.address then - for _, link in ipairs(links) do - fs.remove(link) - end - return false -- remove listener - end -end) \ No newline at end of file diff --git a/src/main/resources/assets/opencomputers/loot/irc/irc.lua b/src/main/resources/assets/opencomputers/loot/irc/bin/irc.lua similarity index 100% rename from src/main/resources/assets/opencomputers/loot/irc/irc.lua rename to src/main/resources/assets/opencomputers/loot/irc/bin/irc.lua diff --git a/src/main/resources/assets/opencomputers/loot/maze/maze.lua b/src/main/resources/assets/opencomputers/loot/maze/bin/maze.lua similarity index 100% rename from src/main/resources/assets/opencomputers/loot/maze/maze.lua rename to src/main/resources/assets/opencomputers/loot/maze/bin/maze.lua diff --git a/src/main/resources/assets/opencomputers/loot/maze/maze.man b/src/main/resources/assets/opencomputers/loot/maze/usr/man/maze.man similarity index 100% rename from src/main/resources/assets/opencomputers/loot/maze/maze.man rename to src/main/resources/assets/opencomputers/loot/maze/usr/man/maze.man diff --git a/src/main/resources/assets/opencomputers/loot/openos/lib/tools/install_utils.lua b/src/main/resources/assets/opencomputers/loot/openos/lib/tools/install_utils.lua index fcce42170..d8db67bc6 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/lib/tools/install_utils.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/lib/tools/install_utils.lua @@ -63,7 +63,7 @@ if cmd == 'select' then elseif cmd == 'install' then local installer_path = options.source_root .. "/.install" - local installer, reason = loadfile(installer_path, "bt", {install= + local installer, reason = loadfile(installer_path, "bt", setmetatable({install= { from=options.source_root, to=options.target_root, @@ -74,7 +74,7 @@ elseif cmd == 'install' then setlabel=options.setlabel, setboot=options.setboot, reboot=options.reboot, - }}) + }}, {__index=_G})) if not installer then io.stderr:write("installer failed to load: " .. tostring(reason) .. '\n') os.exit(1) diff --git a/src/main/resources/assets/opencomputers/loot/oppm/.install b/src/main/resources/assets/opencomputers/loot/oppm/.install new file mode 100644 index 000000000..be8d0d05c --- /dev/null +++ b/src/main/resources/assets/opencomputers/loot/oppm/.install @@ -0,0 +1 @@ +os.execute(install.from.."/oppm.lua") From 636396c6465bd3d4476defc67207aac02a3b9de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Sun, 12 Jun 2016 21:12:32 +0200 Subject: [PATCH 7/7] Beta 3. --- build.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.properties b/build.properties index 9ce007807..b4fff73bd 100644 --- a/build.properties +++ b/build.properties @@ -2,7 +2,7 @@ minecraft.version=1.7.10 forge.version=10.13.4.1448-1.7.10 oc.version=1.6.0 -oc.subversion=beta.2 +oc.subversion=beta.3 ae2.version=rv2-beta-26 bc.version=7.0.9