之前对于文件、用户和磁盘的操作全都在文件系统中实现了。而我们要写的交互界面,将起到操作系统的作用。换句话说它要处理用户的认证、命令解析、打印必要的提示信息、询问命令执行依赖的参数,并最终根据已有的信息调用文件系统或其他代码产生影响。
对于命令,我设计了一种比较便于解析的格式:
- help 打印帮助信息
- exit 退出系统
- user是与用户相关的命令
- user create 创建用户
- user login 用户登录
- ......
- file是与文件相关的命令
- file list 列出用户的文件
- file create 创建文件
- ......
在此基础上,交互界面的大体框架如下:
• package info.skyblond.os.exp.experiment3; • • import info.skyblond.os.exp.experiment3.model.IndexEntry; • import info.skyblond.os.exp.experiment3.model.OpenedFile; • import info.skyblond.os.exp.experiment3.model.User; • import info.skyblond.os.exp.utils.Pair; • • import java.io.File; • import java.nio.charset.StandardCharsets; • import java.util.HashMap; • import java.util.List; • import java.util.Map; • import java.util.Scanner; • import java.util.stream.Collectors; • • • /** • * 文件系统模拟器的主入口 • * 模仿一个简单的操作系统,利用文件系统进行用户鉴权、文件操作 • */ • public class Exp3 { • private static final Simulator simulator = Simulator.getInstance(); • • public static String getPromptLine(String username, String hostname) { • if (username.isBlank()) { • return "nobody@" + hostname + "> "; • } else { • return username + "@" + hostname + "> "; • } • } • • public static void main(String[] args) { • var dataFile = new File("./data.bin"); • simulator.setDataFile(dataFile); • • if (!dataFile.exists()) { • System.out.println("未检测到数据文件,开始初始化。"); • simulator.initDataFile(); • System.out.println("已创建默认用户:root,密码:" + Simulator.ROOT_DEFAULT_PASSWORD); • } • • var hostname = "FSS"; • var currentUser = ""; • • // prompt • System.out.print(getPromptLine(currentUser, hostname)); • Scanner scanner = new Scanner(System.in); • while (scanner.hasNext()) { • String[] line = scanner.nextLine().split(" "); • if (line[0].equalsIgnoreCase("help")) { • // 帮助 • } else if (line[0].equalsIgnoreCase("exit")) { • // 退出 • } else if (line.length == 2) { • // 功能命令 • switch (line[0].toLowerCase()) { • case "user": • // 用户相关命令 • break; • case "file": • // 文件相关命令 • break; • default: • // 未知命令 • break; • } else { • // 未知命令 • } • System.out.print(getPromptLine(currentUser, hostname)); • } • } • }
一上来有一个工具方法 getPromptLine 帮我们拼接出一个提示符,如果当前登录的用户为空,说明没有人登录,就把用户名替换成 nobody,然后在解析命令之前先把这个提示符打印出来,像下面这样:
• nobody@FSS> user login • root@FSS> file list
这样一个优化能够让简陋的命令行一下子上档次不少。
主函数一上来要先准备数据文件,如果数据文件不存在就要初始化一个,并且提供用户默认账户的信息。之后开始命令的解析工作
帮助
这是系统中两个特殊命令之一。通过忽略大小写判断,如果用户在命令行内输入的内容等于 help,那么就打印一个帮助信息,告诉用户这个系统都支持什么命令:
• System.out.println("user list \t\t 列出所有已知用户"); • System.out.println("user create \t 创建新用户(仅限root)"); • System.out.println("user delete \t 删除已有用户(仅限root)"); • System.out.println("user login \t\t 登录(仅未登录时)"); • System.out.println("user logout \t 登出(仅登录时)"); • System.out.println("user chpasswd \t 修改密码(仅登录时)"); • System.out.println("-".repeat(50)); • System.out.println("file list \t\t 列出文件(仅登录时)"); • System.out.println("file create \t 创建文件(仅登录时)"); • System.out.println("file delete \t 删除文件(仅登录时)"); • System.out.println("file open \t\t 打开文件(仅登录时)"); • System.out.println("file close \t\t 关闭文件(仅登录时)"); • System.out.println("file read \t\t 读文件(仅登录时)"); • System.out.println("file write \t\t 写文件(仅登录时)"); • System.out.println("file cutoff \t 截断文件(仅登录时)"); • System.out.println("-".repeat(50)); • System.out.println("help \t\t\t 打印帮助信息"); • System.out.println("exit \t\t\t 退出系统");
就是一堆打印命令。
退出系统
这个是系统中第二个特殊命令。如果用户输入了 exit,那么就退出系统:
• System.out.println("Bye!"); • return;
因为在主函数里,所以直接打印一个 Bye! 之后 return,主函数结束,系统自然就退出了。
功能命令
这部分是实际执行一些操作的命令。由于规定这些命令都是两个单词构成,所以只要判断单词的个数就能确定是这一类命令:
• if (line[0].equalsIgnoreCase("help")) { • // 帮助 • } else if (line[0].equalsIgnoreCase("exit")) { • // 退出 • } else if (line.length == 2) { • // 功能命令 • switch (line[0].toLowerCase()) { • case "user": • // 用户相关命令 • break; • case "file": • // 文件相关命令 • break; • default: • // 未知命令 • System.out.println("未知命令"); • break; • } else { • // 未知命令 • System.out.println("命令格式有误"); • }
所有用户相关的命令都以 user 开头,所有文件相关的命令都由 file 开头,所以直接一个 switch 语句就可以将二者区分开。随后为了提高鲁棒性,加一个 default 处理所有不认识的命令。
用户相关命令
直接看第二个字段就可以判断出命令:
• case "user": • switch (line[1].toLowerCase()) { • case "list": { • // 列出当前所有用户 • break; • } • case "create": { • // 创建 • break; • } • case "delete": { • // 删除 • break; • } • case "login": { • // 登录 • break; • } • case "logout": { • // 登出 • break; • } • case "chpasswd": { • // 修改密码 • break; • } • default: { • System.out.println("未知命令"); • break; • } • } • break;
列出用户
• case "list": { • // 列出当前所有用户 • var userTable = simulator.readUserTable(); • var usernames = userTable.listUsername(); • for (String username : usernames) { • System.out.println(username); • } • break; • }
通过文件系统读出 UserTable,然后列出用户名一一打印即可。
创建用户
• case "create": { • // 创建 • if (currentUser.equals("root")) { • var userTable = simulator.readUserTable(); • var username = ""; • System.out.print("请输入用户名:"); • username = scanner.nextLine(); • if (username.isBlank()) { • System.out.println("用户名不可为空"); • break; • } • if (userTable.get(username) != null) { • System.out.println("用户已存在"); • break; • } • System.out.print("请输入密码:"); • var password = scanner.nextLine(); • var result = simulator.createUser(username, password); • if (result) { • System.out.println("用户创建成功"); • } else { • System.out.println("创建用户失败,请重试"); • } • } else { • System.out.println("请使用root账户登录后重试"); • } • break; • }
创建用户要求 root 账户操作,所以要先检查当前登陆的用户是否为 root。不是则直接进入 else 分支打印提示信息。
对于创建流程来说,询问用户意图创建的用户名,然后检查用户名是否为空、用户名是否已经存在。如果用户名不满足条件,直接结束当前语句的执行。一开始是使用 do..while 来做的,后来认为如果用户中途改变主意不想新增用户了,那么没有办法退出。因此只要不满足条件,直接打印失败信息结束命令。对于密码则没有那么多要求,如果高兴的话密码为空也不是不行。最后调用文件系统创建用户,并根据文件系统的结果打印成功或失败的信息。
删除用户
• case "delete": { • // 删除 • if (currentUser.equals("root")) { • var userTable = simulator.readUserTable(); • var username = ""; • System.out.print("请输入用户名:"); • username = scanner.nextLine(); • if (userTable.get(username) == null) { • System.out.println("用户不存在"); • break; • } • var result = simulator.deleteUser(username); • if (result) { • // 删除成功回收打开的文件 • for (String filename : queryAllOpenedFilename(username)) { • closeFile(username, filename); • } • System.out.println("用户删除成功"); • } else { • System.out.println("删除用户失败,请重试"); • } • } else { • System.out.println("请使用root账户登录后重试"); • } • break; • }
删除用户同样要求 root 操作。如果用户名不存在则直接打印提示并结束命令。否则删除用户,如果删除成功,再回收被删除用户已经打开的文件,这部分内容将在后面打开或关闭文件的时候提到。
用户登录
• case "login": { • // 登录 • if (!currentUser.isBlank()) { • System.out.println("已登录,请先登出"); • } else { • var userTable = simulator.readUserTable(); • User user; • System.out.print("请输入用户名:"); • var username = scanner.nextLine(); • user = userTable.get(username); • if (user == null) { • System.out.println("找不到用户"); • break; • } • System.out.print("请输入用户密码:"); • var password = scanner.nextLine(); • if (user.getPassword().equals(password)) { • currentUser = user.getUsername(); • System.out.println("登陆成功"); • } else { • System.out.println("密码错误,请重试"); • } • } • break; • }
对于用户登录,在逻辑上已经登陆的用户不应该再次登录,所以只有已登录用户名为空的时候才允许登录。首先询问用户名,如果找不到用户则直接结束。找到了就继续询问密码,如果输入的密码和记录的密码相匹配,则成功登录。顺带一说:这里使用的 equals 方法来判断相等,实践上并不安全。一般来说使用 XOR 做对比,如果结果全 0 就说明相等,它与 equals 的区别在于后者会在第一个不相等的地方返回,这样攻击者可以根据运算时间的变化猜测正确的密码,而 XOR 则是要全部过一遍才能得出结果,从运行时间上并不能判断出是哪一位出错。但是由于这个系统偏向演示,所以只用 equals 就足以了。
用户登出
• case "logout": { • // 登出 • if (currentUser.isBlank()) { • System.out.println("请先登录"); • } else { • currentUser = ""; • } • break; • }
有登录就得有登出,登出的话就得先登录。具体操作就是把当前已登录的用户名置为空。
修改密码
• case "chpasswd": { • // 修改密码 • if (currentUser.isBlank()) { • System.out.println("请先登录"); • } else { • var userTable = simulator.readUserTable(); • User user = userTable.get(currentUser); • if (user == null) { • System.out.println("未找到当前已登录用户信息,已自动登出,请重新登陆后尝试"); • currentUser = ""; • break; • } else { • var password = ""; • System.out.print("请输入旧密码:"); • password = scanner.nextLine(); • if (!user.getPassword().equals(password)) { • System.out.println("旧密码错误,请重试"); • break; • } • System.out.print("请输入新密码:"); • password = scanner.nextLine(); • var result = simulator.changePassword(user.getUsername(), password); • if (result) { • System.out.println("修改密码成功"); • } else { • System.out.println("修改密码失败,请重试"); • } • } • }
虽然实验要求没说要有改密码的功能,但代码都写了,不做浪费了。改密码也很简单,首先得要求用户登录,其次要验证旧密码。为了避免意料之外的事情,先要确保当前登录的用户是存在的,如果不存在直接自动登出。旧密码验证失败也直接结束命令的执行。旧密码验证成功之后,询问新密码并调用文件系统做出相应修改。
文件相关命令
文件相关的命令会有一些共同的操作,例如获取当前用户的目录文件 Inode 等,因此在开始指令之前,先把这些公用的东西获取出来:
• case "file": • if (currentUser.isBlank()) { • System.out.println("请先登录"); • } else { • var user = simulator.readUserTable().get(currentUser); • if (user == null) { • System.out.println("找不到当前登录用户的信息,已自动登出,请重新登陆后重试"); • currentUser = ""; • break; • } • var homeInode = simulator.readInodeTable().get(user.getHomeInodeIndex()); • • switch (line[1].toLowerCase()) { • case "list": { • // 列出当前所文件 • break; • } • case "create": { • // 创建一个空文件 • break; • } • case "delete": { • // 删除一个文件 • break; • } • case "open": { • // 打开一个文件 • break; • } • case "close": { • // 关闭一个文件 • break; • } • case "read": { • // 读一个文件 • break; • } • case "write": { • // 写一个文件 • break; • } • case "cutoff": { • // 截断一个文件 • break; • } • default: • System.out.println("未知命令"); • break; • } • } • break;
如果用户当前还没有登陆,使用文件相关的命令时会直接提示。如果用户的 Inode 找不到,说明用户不存在,直接自动登出并提示用户。
之后开始正常的命令解析:
列出文件
• case "list": { • // 列出当前所文件 • var indexEntry = new IndexEntry(); • var entryCount = homeInode.getSize() / indexEntry.byteCount(); • var openedFilename = queryAllOpenedFilename(currentUser); • var inodeTable = simulator.readInodeTable(); • simulator.readSomeIndexEntry(homeInode, 0, entryCount) • .forEach((f) -> { • System.out.print("文件名:" + f.getFileName()); • if (openedFilename.contains(f.getFileName())) { • System.out.print(",已打开"); • } • System.out.println(",大小" + inodeTable.get(f.getInodeIndex()).getSize() + "B"); • }); • break; • }
列出文件使用读取目录项的方法。同时为了标记已经打开的文件,还会查询该用户所有已经打开的文件名并加以标注。最后每个文件还会打印它的大小。
创建文件
• case "create": { • // 创建一个空文件 • var filename = ""; • System.out.print("请输入文件名:"); • filename = scanner.nextLine(); • if (filename.isBlank()) { • System.out.println("文件名不可为空"); • break; • } • if (simulator.containsFile(homeInode, filename)) { • System.out.println("文件已存在"); • break; • } • var result = simulator.createFile(currentUser, filename); • if (result) { • System.out.println("创建成功"); • } else { • System.out.println("创建失败"); • } • break; • }
创建文件默认创建一个空白文件,不分配任何盘块。同样还是询问文件名,如果文件名已经存在或者为空,直接结束命令执行。通过检查则调用文件系统创建文件,并根据结果打印相关信息。
删除文件
• case "delete": { • // 删除一个文件 • var filename = ""; • System.out.print("请输入文件名:"); • filename = scanner.nextLine(); • if (!simulator.containsFile(homeInode, filename)) { • System.out.println("文件不存在"); • break; • } • • if (queryOpenedFile(currentUser, filename) != null) { • System.out.println("文件已打开,请先关闭再删除"); • break; • } • • var result = simulator.deleteFile(currentUser, filename); • if (result) { • System.out.println("删除成功"); • } else { • System.out.println("删除失败"); • } • break; • }
删除文件也是一样,如果文件不存在,或者已经打开,则拒绝删除。否则调用文件系统删除文件。
打开文件
• case "open": { • // 打开一个文件 • var filename = ""; • System.out.print("请输入文件名:"); • filename = scanner.nextLine(); • if (!simulator.containsFile(homeInode, filename)) { • System.out.println("文件不存在"); • break; • } • • if (queryOpenedFile(currentUser, filename) != null) { • System.out.println("文件已打开"); • break; • } • • var result = openFile(currentUser, filename); • if (result) { • System.out.println("文件打开成功"); • } else { • System.out.println("文件打开失败"); • } • break; • }
打开文件只允许打开已经存在且尚未打开的文件。相关工具方法如下
• /** • * 打开的文件,((用户名,文件名),文件描述) • */ • private static final Map<Pair<String, String>, OpenedFile> openedFiles = new HashMap<>(); • • /** • * 打开文件 • */ • public static boolean openFile(String username, String filename) { • var user = simulator.readUserTable().get(username); • if (user == null) { • return false; • } • var inodeTable = simulator.readInodeTable(); • var homeInode = inodeTable.get(user.getHomeInodeIndex()); • • // 不存在的文件直接返回false • if (!simulator.containsFile(homeInode, filename)) { • return false; • } • • // 如果已经打开也直接返回 • if (openedFiles.containsKey(new Pair<>(username, filename))) { • return false; • } • • openedFiles.put(new Pair<>(username, filename), new OpenedFile(simulator.findInodeByFilename(homeInode, filename))); • return true; • } • • /** • * 关闭文件 • */ • public static boolean closeFile(String username, String filename) { • // 如果没打开直接返回 • if (!openedFiles.containsKey(new Pair<>(username, filename))) { • return false; • } • • openedFiles.remove(new Pair<>(username, filename)); • return true; • } • • /** • * 通过用户名和文件名查询已打开的文件描述 • */ • public static OpenedFile queryOpenedFile(String username, String filename) { • // 如果没打开直接返回 • if (!openedFiles.containsKey(new Pair<>(username, filename))) { • return null; • } • return openedFiles.get(new Pair<>(username, filename)); • } • • /** • * 获取已经打开的文件名 • */ • public static List<String> queryAllOpenedFilename(String username) { • return openedFiles.keySet().stream().filter(p -> p.getFirst().equals(username)).map(Pair::getSecond).collect(Collectors.toList()); • }
打开文件的核心就是将打开的文件信息存储在内存中。打开的文件信息包括文件的 Inode 引用和一个当前读写指针的位置。后者将提供随机访问的特性。
关闭文件
• case "close": { • // 关闭一个文件 • var filename = ""; • System.out.print("请输入文件名:"); • filename = scanner.nextLine(); • if (queryOpenedFile(currentUser, filename) == null) { • System.out.println("文件未打开"); • break; • } • • var result = closeFile(currentUser, filename); • if (result) { • System.out.println("文件关闭成功"); • } else { • System.out.println("文件关闭失败"); • } • break; • }
文件打开了就得关闭,要不然会造成内存泄漏。关闭只允许关闭已经打开的文件。操作上就是将对应的信息从内存中移除。
读文件
• case "read": { • // 读一个文件 • var filename = ""; • System.out.print("请输入文件名:"); • filename = scanner.nextLine(); • var openedFile = queryOpenedFile(currentUser, filename); • if (openedFile == null) { • System.out.println("文件未打开"); • break; • } • • var delta = 0L; • System.out.println("当前文件描述符指针在" + openedFile.getPos()); • System.out.println("文件大小" + openedFile.getInode().getSize() + "B"); • if (openedFile.getPos() >= openedFile.getInode().getSize()) { • System.out.println("已到达文件尾。"); • } • System.out.print("请输入相对偏移量:"); • delta = scanner.nextLong(); • scanner.nextLine(); • // 更新偏移量 • openedFile.setPos(openedFile.getPos() + delta); • System.out.println("移动后指针:" + openedFile.getPos()); • var length = 0L; • System.out.print("请输入要读取的长度:"); • length = scanner.nextLong(); • scanner.nextLine(); • • if (length <= 0) { • System.out.println("长度必须大于0"); • } • • try { • var bytes = simulator.readFile(openedFile.getInode(), openedFile.getPos(), length); • System.out.println(new String(bytes, StandardCharsets.UTF_8)); • openedFile.setPos(openedFile.getPos() + bytes.length); • System.out.println("读取后文件指针:" + openedFile.getPos()); • } catch (Exception e) { • System.out.println("读取失败:" + e.getMessage()); • } • break; • }
读文件要求只能读打开的文件,原因是未打开的文件没有读写指针,不知道从哪里开始读。读取的时候会根据提供的文件名寻找已打开的文件信息,并提示用户操作读写指针进行随机读写。
由于这个系统很简陋,所以唯一能读写的东西就是字符。在读取部分简单的包了个异常处理,因为文件系统那里会对传入参数进行检查,如果检查失败直接抛出异常崩溃 JVM,这个异常处理要做的就是拦截异常,将其信息打印给用户,然后结束指令。
写文件
• case "write": { • // 写一个文件 • var filename = ""; • System.out.print("请输入文件名:"); • filename = scanner.nextLine(); • var openedFile = queryOpenedFile(currentUser, filename); • if (openedFile == null) { • System.out.println("文件未打开"); • break; • } • • var delta = 0L; • System.out.println("当前文件描述符指针在" + openedFile.getPos()); • System.out.println("文件大小" + openedFile.getInode().getSize() + "B"); • if (openedFile.getPos() >= openedFile.getInode().getSize()) { • System.out.println("已到达文件尾"); • } • System.out.print("请输入相对偏移量:"); • delta = scanner.nextLong(); • scanner.nextLine(); • // 更新偏移量 • openedFile.setPos(openedFile.getPos() + delta); • System.out.println("移动后指针:" + openedFile.getPos()); • var length = 0L; • • System.out.println("请在下方输入要写入的内容,单独一行'$EOF'表示结束:"); • // 打印一个字符避免退格时回到已经写入的上一行 • System.out.print(">"); • var inputLine = scanner.nextLine(); • try { • • while (!inputLine.equals("$EOF")) { • if (length != 0) { • // 不是第一行,向前面行追加回车 • inputLine = String.format("\n%s", inputLine); • } • • var bytes = inputLine.getBytes(StandardCharsets.UTF_8); • var result = simulator.writeFile(openedFile.getInode(), openedFile.getPos() + length, bytes); • • if (!result) { • System.out.println("写入失败!"); • break; • } • • // 更新长度 • length += bytes.length; • // 读下一行 • System.out.print(">"); • inputLine = scanner.nextLine(); • } • } catch (Exception e) { • System.out.println("写入失败:" + e.getMessage()); • } • System.out.println("已写入" + length + "B"); • openedFile.setPos(openedFile.getPos() + length); • System.out.println("当前文件指针:" + openedFile.getPos()); • break; • }
写文件的交互与读文件的类似。写入支持多行,单行的 $EOF 表示结束。如果中途写入失败,通常是指针越界,文件系统会直接抛出异常。这个异常同样会被异常处理拦截并转换成错误信息。
截断文件
• case "cutoff": { • // 截断一个文件 • var filename = ""; • System.out.print("请输入文件名:"); • filename = scanner.nextLine(); • var openedFile = queryOpenedFile(currentUser, filename); • if (openedFile == null) { • System.out.println("文件未打开"); • break; • } • • System.out.println("文件大小" + openedFile.getInode().getSize() + "B"); • if (openedFile.getPos() >= openedFile.getInode().getSize()) { • System.out.println("已到达文件尾"); • } • var newSize = 0L; • System.out.print("请输入新的大小:"); • newSize = scanner.nextLong(); • scanner.nextLine(); • • if (newSize > openedFile.getInode().getSize()) { • System.out.println("新大小不能超过原来的大小"); • break; • } • • simulator.cutoffFile(openedFile.getInode(), newSize); • • System.out.println("截断文件成功"); • break; • }
就是询问文件的新大小,并调用文件系统的相关功能。
效果
• 未检测到数据文件,开始初始化。 • 已创建默认用户:root,密码:password • nobody@FSS> user login • 请输入用户名:root • 请输入用户密码:123 • 密码错误,请重试 • nobody@FSS> user login • 请输入用户名:root • 请输入用户密码:password • 登陆成功 • root@FSS> file list • root@FSS> file create • 请输入文件名:123 • 创建成功 • root@FSS> file open • 请输入文件名:123 • 文件打开成功 • root@FSS> file list • 文件名:123,已打开,大小0B • root@FSS> file write • 请输入文件名:123 • 当前文件描述符指针在0 • 文件大小0B • 已到达文件尾 • 请输入相对偏移量:1 • 移动后指针:1 • 请在下方输入要写入的内容,单独一行'$EOF'表示结束: • >123 • 写入失败:Offset must not bigger than size. • 已写入0B • 当前文件指针:1 • root@FSS> file write • 请输入文件名:123 • 当前文件描述符指针在1 • 文件大小0B • 已到达文件尾 • 请输入相对偏移量:-1 • 移动后指针:0 • 请在下方输入要写入的内容,单独一行'$EOF'表示结束: • >1234567890 • >abc • > • >$EOF • 已写入15B • 当前文件指针:15 • root@FSS> file read • 请输入文件名:123 • 当前文件描述符指针在15 • 文件大小15B • 已到达文件尾。 • 请输入相对偏移量:-15 • 移动后指针:0 • 请输入要读取的长度:15 • 1234567890 • abc • • 读取后文件指针:15 • root@FSS> file close • 请输入文件名:1234 • 文件未打开 • root@FSS> file close • 请输入文件名:123 • 文件关闭成功 • root@FSS> exit • Bye!
这个效果还不错,至少我简单的测试了一下,没有意料之外的报错。后来请同学以「让这个系统崩溃」为目标可劲儿的尝试,最终看起来代码没有比较严重的 bug,也没有与预期不相符的行为。
总结展开目录
今天程序顺利通过验机,这个系列也就此告一段落。这个实验在 11 月 3 号开始写,到 12 月 3 号写完,正好一个月。实话说这个程序虽然实用性不强,但是其中用到的许多设计想法都是我未曾在实用程序中实践过的。例如用户输入的条件不满足直接抛异常崩 JVM,我大概永远不敢在重要的程序上这样做,通常有一个 default 会有用很多。另外一个例子就是整个 Wrapped 的构造,平时写代码的时候别说字节数组了,Serializable 都很少用,直接一个 Gson 或者 Jackson,反射就把这事儿做了,我也无须关心底层是怎么实现的(当然了,用了 Gson 打完包程序上百 MB 就是另一个故事了)。以及直接使用 RandomAccessFile 像文件系统那样将数据定位到字节的精度去操作,这种深度还是我头一次用 Java 做。
无论如何,这个项目很顺利的完成了。本文作为这个系列的最后一篇文章,实现了人机交互界面,并最终让这个系统运行了起来。
附录:完整代码
• package info.skyblond.os.exp.experiment3; • • import info.skyblond.os.exp.experiment3.model.IndexEntry; • import info.skyblond.os.exp.experiment3.model.OpenedFile; • import info.skyblond.os.exp.experiment3.model.User; • import info.skyblond.os.exp.utils.Pair; • • import java.io.File; • import java.nio.charset.StandardCharsets; • import java.util.HashMap; • import java.util.List; • import java.util.Map; • import java.util.Scanner; • import java.util.stream.Collectors; • • • /** • * 文件系统模拟器的主入口 • * 模仿一个简单的操作系统,利用文件系统进行用户鉴权、文件操作 • */ • public class Exp3 { • private static final Simulator simulator = Simulator.getInstance(); • • /** • * 打开的文件,((用户名,文件名),文件描述) • */ • private static final Map<Pair<String, String>, OpenedFile> openedFiles = new HashMap<>(); • • • /** • * 打开文件 • */ • public static boolean openFile(String username, String filename) { • var user = simulator.readUserTable().get(username); • if (user == null) { • return false; • } • var inodeTable = simulator.readInodeTable(); • var homeInode = inodeTable.get(user.getHomeInodeIndex()); • • // 不存在的文件直接返回false • if (!simulator.containsFile(homeInode, filename)) { • return false; • } • • // 如果已经打开也直接返回 • if (openedFiles.containsKey(new Pair<>(username, filename))) { • return false; • } • • openedFiles.put(new Pair<>(username, filename), new OpenedFile(simulator.findInodeByFilename(homeInode, filename))); • return true; • } • • /** • * 关闭文件 • */ • public static boolean closeFile(String username, String filename) { • // 如果没打开直接返回 • if (!openedFiles.containsKey(new Pair<>(username, filename))) { • return false; • } • • openedFiles.remove(new Pair<>(username, filename)); • return true; • } • • /** • * 通过用户名和文件名查询已打开的文件描述 • */ • public static OpenedFile queryOpenedFile(String username, String filename) { • // 如果没打开直接返回 • if (!openedFiles.containsKey(new Pair<>(username, filename))) { • return null; • } • return openedFiles.get(new Pair<>(username, filename)); • } • • /** • * 获取已经打开的文件名 • */ • public static List<String> queryAllOpenedFilename(String username) { • return openedFiles.keySet().stream().filter(p -> p.getFirst().equals(username)).map(Pair::getSecond).collect(Collectors.toList()); • } • • • public static String getPromptLine(String username, String hostname) { • if (username.isBlank()) { • return "nobody@" + hostname + "> "; • } else { • return username + "@" + hostname + "> "; • } • } • • public static void main(String[] args) { • var dataFile = new File("./data.bin"); • simulator.setDataFile(dataFile); • • if (!dataFile.exists()) { • System.out.println("未检测到数据文件,开始初始化。"); • simulator.initDataFile(); • System.out.println("已创建默认用户:root,密码:" + Simulator.ROOT_DEFAULT_PASSWORD); • } • • var hostname = "FSS"; • // var currentUser = "root"; // 默认root用户,调试用 • var currentUser = ""; • • // prompt • System.out.print(getPromptLine(currentUser, hostname)); • Scanner scanner = new Scanner(System.in); • while (scanner.hasNext()) { • String[] line = scanner.nextLine().split(" "); • if (line[0].equalsIgnoreCase("help")) { • // 帮助 • System.out.println("user list \t\t 列出所有已知用户"); • System.out.println("user create \t 创建新用户(仅限root)"); • System.out.println("user delete \t 删除已有用户(仅限root)"); • System.out.println("user login \t\t 登录(仅未登录时)"); • System.out.println("user logout \t 登出(仅登录时)"); • System.out.println("user chpasswd \t 修改密码(仅登录时)"); • System.out.println("-".repeat(50)); • System.out.println("file list \t\t 列出文件(仅登录时)"); • System.out.println("file create \t 创建文件(仅登录时)"); • System.out.println("file delete \t 删除文件(仅登录时)"); • System.out.println("file open \t\t 打开文件(仅登录时)"); • System.out.println("file close \t\t 关闭文件(仅登录时)"); • System.out.println("file read \t\t 读文件(仅登录时)"); • System.out.println("file write \t\t 写文件(仅登录时)"); • System.out.println("file cutoff \t 截断文件(仅登录时)"); • System.out.println("-".repeat(50)); • System.out.println("help \t\t\t 打印帮助信息"); • System.out.println("exit \t\t\t 退出系统"); • } else if (line[0].equalsIgnoreCase("exit")) { • // 退出 • System.out.println("Bye!"); • return; • } else if (line.length == 2) { • switch (line[0].toLowerCase()) { • case "user": • switch (line[1].toLowerCase()) { • case "list": { • // 列出当前所有用户 • var userTable = simulator.readUserTable(); • var usernames = userTable.listUsername(); • for (String username : usernames) { • System.out.println(username); • } • break; • } • case "create": { • // 创建 • if (currentUser.equals("root")) { • var userTable = simulator.readUserTable(); • var username = ""; • System.out.print("请输入用户名:"); • username = scanner.nextLine(); • if (username.isBlank()) { • System.out.println("用户名不可为空"); • break; • } • if (userTable.get(username) != null) { • System.out.println("用户已存在"); • break; • } • System.out.print("请输入密码:"); • var password = scanner.nextLine(); • var result = simulator.createUser(username, password); • if (result) { • System.out.println("用户创建成功"); • } else { • System.out.println("创建用户失败,请重试"); • } • } else { • System.out.println("请使用root账户登录后重试"); • } • break; • } • case "delete": { • // 删除 • if (currentUser.equals("root")) { • var userTable = simulator.readUserTable(); • var username = ""; • System.out.print("请输入用户名:"); • username = scanner.nextLine(); • if (userTable.get(username) == null) { • System.out.println("用户不存在"); • break; • } • var result = simulator.deleteUser(username); • if (result) { • // 删除成功回收打开的文件 • for (String filename : queryAllOpenedFilename(username)) { • closeFile(username, filename); • } • System.out.println("用户删除成功"); • } else { • System.out.println("删除用户失败,请重试"); • } • } else { • System.out.println("请使用root账户登录后重试"); • } • break; • } • case "login": { • // 登录 • if (!currentUser.isBlank()) { • System.out.println("已登录,请先登出"); • } else { • var userTable = simulator.readUserTable(); • User user; • System.out.print("请输入用户名:"); • var username = scanner.nextLine(); • user = userTable.get(username); • if (user == null) { • System.out.println("找不到用户"); • break; • } • System.out.print("请输入用户密码:"); • var password = scanner.nextLine(); • if (user.getPassword().equals(password)) { • currentUser = user.getUsername(); • System.out.println("登陆成功"); • } else { • System.out.println("密码错误,请重试"); • } • } • break; • } • case "logout": { • // 登出 • if (currentUser.isBlank()) { • System.out.println("请先登录"); • } else { • currentUser = ""; • } • break; • } • case "chpasswd": { • // 修改密码 • if (currentUser.isBlank()) { • System.out.println("请先登录"); • } else { • var userTable = simulator.readUserTable(); • User user = userTable.get(currentUser); • if (user == null) { • System.out.println("未找到当前已登录用户信息,已自动登出,请重新登陆后尝试"); • currentUser = ""; • break; • } else { • var password = ""; • System.out.print("请输入旧密码:"); • password = scanner.nextLine(); • if (!user.getPassword().equals(password)) { • System.out.println("旧密码错误,请重试"); • break; • } • System.out.print("请输入新密码:"); • password = scanner.nextLine(); • var result = simulator.changePassword(user.getUsername(), password); • if (result) { • System.out.println("修改密码成功"); • } else { • System.out.println("修改密码失败,请重试"); • } • } • } • break; • } • default: { • System.out.println("未知命令"); • break; • } • } • break; • case "file": • if (currentUser.isBlank()) { • System.out.println("请先登录"); • } else { • var user = simulator.readUserTable().get(currentUser); • if (user == null) { • System.out.println("找不到当前登录用户的信息,已自动登出,请重新登陆后重试"); • currentUser = ""; • break; • } • var homeInode = simulator.readInodeTable().get(user.getHomeInodeIndex()); • • switch (line[1].toLowerCase()) { • case "list": { • // 列出当前所文件 • var indexEntry = new IndexEntry(); • var entryCount = homeInode.getSize() / indexEntry.byteCount(); • var openedFilename = queryAllOpenedFilename(currentUser); • var inodeTable = simulator.readInodeTable(); • simulator.readSomeIndexEntry(homeInode, 0, entryCount) • .forEach((f) -> { • System.out.print("文件名:" + f.getFileName()); • if (openedFilename.contains(f.getFileName())) { • System.out.print(",已打开"); • } • System.out.println(",大小" + inodeTable.get(f.getInodeIndex()).getSize() + "B"); • }); • break; • } • case "create": { • // 创建一个空文件 • var filename = ""; • System.out.print("请输入文件名:"); • filename = scanner.nextLine(); • if (filename.isBlank()) { • System.out.println("文件名不可为空"); • break; • } • if (simulator.containsFile(homeInode, filename)) { • System.out.println("文件已存在"); • break; • } • var result = simulator.createFile(currentUser, filename); • if (result) { • System.out.println("创建成功"); • } else { • System.out.println("创建失败"); • } • break; • } • case "delete": { • // 删除一个文件 • var filename = ""; • System.out.print("请输入文件名:"); • filename = scanner.nextLine(); • if (!simulator.containsFile(homeInode, filename)) { • System.out.println("文件不存在"); • break; • } • • if (queryOpenedFile(currentUser, filename) != null) { • System.out.println("文件已打开,请先关闭再删除"); • break; • } • • var result = simulator.deleteFile(currentUser, filename); • if (result) { • System.out.println("删除成功"); • } else { • System.out.println("删除失败"); • } • break; • } • case "open": { • // 打开一个文件 • var filename = ""; • System.out.print("请输入文件名:"); • filename = scanner.nextLine(); • if (!simulator.containsFile(homeInode, filename)) { • System.out.println("文件不存在"); • break; • } • • if (queryOpenedFile(currentUser, filename) != null) { • System.out.println("文件已打开"); • break; • } • • var result = openFile(currentUser, filename); • if (result) { • System.out.println("文件打开成功"); • } else { • System.out.println("文件打开失败"); • } • break; • } • case "close": { • // 关闭一个文件 • var filename = ""; • System.out.print("请输入文件名:"); • filename = scanner.nextLine(); • if (queryOpenedFile(currentUser, filename) == null) { • System.out.println("文件未打开"); • break; • } • • var result = closeFile(currentUser, filename); • if (result) { • System.out.println("文件关闭成功"); • } else { • System.out.println("文件关闭失败"); • } • break; • } • case "read": { • // 读一个文件 • var filename = ""; • System.out.print("请输入文件名:"); • filename = scanner.nextLine(); • var openedFile = queryOpenedFile(currentUser, filename); • if (openedFile == null) { • System.out.println("文件未打开"); • break; • } • • var delta = 0L; • System.out.println("当前文件描述符指针在" + openedFile.getPos()); • System.out.println("文件大小" + openedFile.getInode().getSize() + "B"); • if (openedFile.getPos() >= openedFile.getInode().getSize()) { • System.out.println("已到达文件尾。"); • } • System.out.print("请输入相对偏移量:"); • delta = scanner.nextLong(); • scanner.nextLine(); • // 更新偏移量 • openedFile.setPos(openedFile.getPos() + delta); • System.out.println("移动后指针:" + openedFile.getPos()); • var length = 0L; • System.out.print("请输入要读取的长度:"); • length = scanner.nextLong(); • scanner.nextLine(); • • if (length <= 0) { • System.out.println("长度必须大于0"); • } • • try { • var bytes = simulator.readFile(openedFile.getInode(), openedFile.getPos(), length); • System.out.println(new String(bytes, StandardCharsets.UTF_8)); • openedFile.setPos(openedFile.getPos() + bytes.length); • System.out.println("读取后文件指针:" + openedFile.getPos()); • } catch (Exception e) { • System.out.println("读取失败:" + e.getMessage()); • } • break; • } • case "write": { • // 写一个文件 • var filename = ""; • System.out.print("请输入文件名:"); • filename = scanner.nextLine(); • var openedFile = queryOpenedFile(currentUser, filename); • if (openedFile == null) { • System.out.println("文件未打开"); • break; • } • • var delta = 0L; • System.out.println("当前文件描述符指针在" + openedFile.getPos()); • System.out.println("文件大小" + openedFile.getInode().getSize() + "B"); • if (openedFile.getPos() >= openedFile.getInode().getSize()) { • System.out.println("已到达文件尾"); • } • System.out.print("请输入相对偏移量:"); • delta = scanner.nextLong(); • scanner.nextLine(); • // 更新偏移量 • openedFile.setPos(openedFile.getPos() + delta); • System.out.println("移动后指针:" + openedFile.getPos()); • var length = 0L; • • System.out.println("请在下方输入要写入的内容,单独一行'$EOF'表示结束:"); • // 打印一个字符避免退格时回到已经写入的上一行 • System.out.print(">"); • var inputLine = scanner.nextLine(); • try { • • while (!inputLine.equals("$EOF")) { • if (length != 0) { • // 不是第一行,向前面行追加回车 • inputLine = String.format("\n%s", inputLine); • } • • var bytes = inputLine.getBytes(StandardCharsets.UTF_8); • var result = simulator.writeFile(openedFile.getInode(), openedFile.getPos() + length, bytes); • • if (!result) { • System.out.println("写入失败!"); • break; • } • • // 更新长度 • length += bytes.length; • // 读下一行 • System.out.print(">"); • inputLine = scanner.nextLine(); • } • } catch (Exception e) { • System.out.println("写入失败:" + e.getMessage()); • } • System.out.println("已写入" + length + "B"); • openedFile.setPos(openedFile.getPos() + length); • System.out.println("当前文件指针:" + openedFile.getPos()); • break; • } • case "cutoff": { • // 截断一个文件 • var filename = ""; • System.out.print("请输入文件名:"); • filename = scanner.nextLine(); • var openedFile = queryOpenedFile(currentUser, filename); • if (openedFile == null) { • System.out.println("文件未打开"); • break; • } • • System.out.println("文件大小" + openedFile.getInode().getSize() + "B"); • if (openedFile.getPos() >= openedFile.getInode().getSize()) { • System.out.println("已到达文件尾"); • } • var newSize = 0L; • System.out.print("请输入新的大小:"); • newSize = scanner.nextLong(); • scanner.nextLine(); • • if (newSize > openedFile.getInode().getSize()) { • System.out.println("新大小不能超过原来的大小"); • break; • } • • simulator.cutoffFile(openedFile.getInode(), newSize); • • System.out.println("截断文件成功"); • break; • } • default: • System.out.println("未知命令"); • break; • } • } • break; • default: • System.out.println("未知命令"); • break; • } • } else { • System.out.println("命令格式有误"); • } • • System.out.print(getPromptLine(currentUser, hostname)); • } • } • }