mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-03-07 19:51:51 +09:00
567 lines
20 KiB
JavaScript
567 lines
20 KiB
JavaScript
let PROMPT_TEXT = ">";
|
|
let CURRENT_DRIVE = "A";
|
|
let shell_pwd = [""];
|
|
|
|
let goInteractive = false;
|
|
let goFancy = false;
|
|
|
|
let DEBUG_PRINT = true;
|
|
|
|
let errorlevel = 0;
|
|
|
|
const termWidth = con.getmaxyx()[1];
|
|
const termHeight = con.getmaxyx()[0];
|
|
const welcome_text = (termWidth > 40) ? "TSVM 한글 DOS, 버전 " + _TVDOS.VERSION
|
|
: "TSVM 한글 DOS " + _TVDOS.VERSION;
|
|
const greetLeftPad = (termWidth - welcome_text.length - 6) >> 1;
|
|
const greetRightPad = termWidth - greetLeftPad - welcome_text.length - 6;
|
|
|
|
function makeHash() {
|
|
let e = "YBNDRFG8EJKMCPQXOTLVWIS2A345H769";
|
|
let m = e.length;
|
|
return e[Math.floor(Math.random()*m)] + e[Math.floor(Math.random()*m)] + e[Math.floor(Math.random()*m)] + e[Math.floor(Math.random()*m)] + e[Math.floor(Math.random()*m)]
|
|
}
|
|
|
|
const shellID = makeHash();
|
|
|
|
function print_prompt_text() {
|
|
if (goFancy) {
|
|
con.color_pair(239,161);
|
|
print(" "+CURRENT_DRIVE+":");
|
|
con.color_pair(161,253);
|
|
con.addch(16);con.curs_right();
|
|
con.color_pair(0,253);
|
|
print(" /"+shell_pwd.join("/").substring(1)+" ");
|
|
if (errorlevel != 0) {
|
|
con.color_pair(166,253);
|
|
print("["+errorlevel+"] ");
|
|
}
|
|
con.color_pair(253,255);
|
|
con.addch(16);con.curs_right();
|
|
con.addch(32);con.curs_right();
|
|
con.color_pair(253,255);
|
|
}
|
|
else {
|
|
// con.color_pair(253,255);
|
|
if (errorlevel != 0)
|
|
print(CURRENT_DRIVE + ":/" + shell_pwd.join("/") + " [" + errorlevel + "]" + PROMPT_TEXT);
|
|
else
|
|
print(CURRENT_DRIVE + ":/" + shell_pwd.join("/") + PROMPT_TEXT);
|
|
}
|
|
}
|
|
|
|
function greet() {
|
|
if (goFancy) {
|
|
con.color_pair(239,255);
|
|
con.clear();
|
|
con.color_pair(253,255);
|
|
print(' ');con.addch(17);con.curs_right();
|
|
con.color_pair(0,253);
|
|
print(" ".repeat(greetLeftPad)+welcome_text+" ".repeat(greetRightPad));
|
|
con.color_pair(253,255);
|
|
con.addch(16);con.curs_right();print(' ');
|
|
con.move(3,1);
|
|
}
|
|
else
|
|
println(welcome_text);
|
|
}
|
|
|
|
/*uninterruptible*/ function sendLcdMsg(s) {
|
|
// making it uninterruptible
|
|
sys.poke(-1025, (s === undefined) ? 0 : s.charCodeAt(0)|0);
|
|
sys.poke(-1026, (s === undefined) ? 0 : s.charCodeAt(1)|0);
|
|
sys.poke(-1027, (s === undefined) ? 0 : s.charCodeAt(2)|0);
|
|
sys.poke(-1028, (s === undefined) ? 0 : s.charCodeAt(3)|0);
|
|
sys.poke(-1029, (s === undefined) ? 0 : s.charCodeAt(4)|0);
|
|
sys.poke(-1030, (s === undefined) ? 0 : s.charCodeAt(5)|0);
|
|
sys.poke(-1031, (s === undefined) ? 0 : s.charCodeAt(6)|0);
|
|
sys.poke(-1032, (s === undefined) ? 0 : s.charCodeAt(7)|0);
|
|
sys.poke(-1033, (s === undefined) ? 0 : s.charCodeAt(8)|0);
|
|
sys.poke(-1034, (s === undefined) ? 0 : s.charCodeAt(9)|0);
|
|
sys.poke(-1035, (s === undefined) ? 0 : s.charCodeAt(10)|0);
|
|
sys.poke(-1036, (s === undefined) ? 0 : s.charCodeAt(11)|0);
|
|
sys.poke(-1037, (s === undefined) ? 0 : s.charCodeAt(12)|0);
|
|
sys.poke(-1038, (s === undefined) ? 0 : s.charCodeAt(13)|0);
|
|
sys.poke(-1039, (s === undefined) ? 0 : s.charCodeAt(14)|0);
|
|
sys.poke(-1040, (s === undefined) ? 0 : s.charCodeAt(15)|0);
|
|
sys.poke(-1041, (s === undefined) ? 0 : s.charCodeAt(16)|0);
|
|
sys.poke(-1042, (s === undefined) ? 0 : s.charCodeAt(17)|0);
|
|
sys.poke(-1043, (s === undefined) ? 0 : s.charCodeAt(18)|0);
|
|
sys.poke(-1044, (s === undefined) ? 0 : s.charCodeAt(19)|0);
|
|
sys.poke(-1045, (s === undefined) ? 0 : s.charCodeAt(20)|0);
|
|
sys.poke(-1046, (s === undefined) ? 0 : s.charCodeAt(21)|0);
|
|
sys.poke(-1047, (s === undefined) ? 0 : s.charCodeAt(22)|0);
|
|
sys.poke(-1048, (s === undefined) ? 0 : s.charCodeAt(23)|0);
|
|
}
|
|
|
|
function trimStartRevSlash(s) {
|
|
var cnt = 0;
|
|
while (cnt < s.length) {
|
|
var chr = s[cnt];
|
|
|
|
if (chr != '\\') break;
|
|
|
|
cnt += 1;
|
|
}
|
|
|
|
return s.substring(cnt);
|
|
}
|
|
|
|
let shell = {};
|
|
shell.replaceVarCall = function(value) {
|
|
// syntax:
|
|
// line = literal [varcall] [literal] ;
|
|
// varcall = "$" ident ;
|
|
// ident = ? regex: [A-Za-z_]+ ? ;
|
|
// literal = ? you know what it is ? ;
|
|
let replaceMap = [];
|
|
let varMode = false;
|
|
let sb = '';
|
|
for (let i=0; i<value.length; i++) {
|
|
let char = value.charAt(i);
|
|
let cp = value.charCodeAt(i);
|
|
if (!varMode && char == '$') {
|
|
replaceMap.push({s:sb,r:false});
|
|
sb = ''; varMode = true;
|
|
}
|
|
else if (varMode && !(cp >= 48 && cp <= 57 || cp >= 65 && cp <= 90 || cp == 95 || cp >= 97 && cp <= 122)) {
|
|
replaceMap.push({s:sb,r:true});
|
|
sb = ''+char; varMode = false;
|
|
}
|
|
else sb += char;
|
|
}; replaceMap.push({s:sb,r:(varMode)});
|
|
|
|
return replaceMap.map(it => (it.r) ? _TVDOS.variables[it.s] : it.s).join('');
|
|
}
|
|
shell.getPwd = function() { return shell_pwd; }
|
|
shell.getPwdString = function() { return "\\" + (shell_pwd.concat([""])).join("\\"); }
|
|
shell.getCurrentDrive = function() { return CURRENT_DRIVE; }
|
|
// example input: echo "the string" > subdir\test.txt
|
|
shell.parse = function(input) {
|
|
var tokens = [];
|
|
var stringBuffer = "";
|
|
var mode = "LITERAL"; // LITERAL, QUOTE, ESCAPE, LIMBO
|
|
var i = 0
|
|
while (i < input.length) {
|
|
const c = input[i];
|
|
/*digraph g {
|
|
LITERAL -> QUOTE [label="\""]
|
|
LITERAL -> LIMBO [label="space"]
|
|
LITERAL -> LITERAL [label=else]
|
|
|
|
QUOTE -> LIMBO [label="\""]
|
|
QUOTE -> ESCAPE [label="\\"]
|
|
QUOTE -> QUOTE [label=else]
|
|
|
|
ESCAPE -> QUOTE
|
|
|
|
LIMBO -> LITERAL [label="not space"]
|
|
LIMBO -> QUOTE [label="\""]
|
|
LIMBO -> LIMBO [label="space"]
|
|
}*/
|
|
if ("LITERAL" == mode) {
|
|
if (' ' == c) {
|
|
tokens.push(stringBuffer); stringBuffer = "";
|
|
mode = "LIMBO";
|
|
}
|
|
else if ('"' == c) {
|
|
tokens.push(stringBuffer); stringBuffer = "";
|
|
mode = "QUOTE";
|
|
}
|
|
else {
|
|
stringBuffer += c;
|
|
}
|
|
}
|
|
else if ("LIMBO" == mode) {
|
|
if ('"' == c) {
|
|
mode = "QUOTE";
|
|
}
|
|
else if (c != ' ') {
|
|
mode = "LITERAL";
|
|
stringBuffer += c;
|
|
}
|
|
}
|
|
else if ("QUOTE" == mode) {
|
|
if ('"' == c) {
|
|
tokens.push(stringBuffer); stringBuffer = "";
|
|
mode = "LIMBO";
|
|
}
|
|
else if ('^' == c) {
|
|
mode = "ESCAPE";
|
|
}
|
|
else {
|
|
stringBuffer += c;
|
|
}
|
|
}
|
|
else if ("ESCAPE" == mode) {
|
|
TODO();
|
|
}
|
|
|
|
i += 1;
|
|
}
|
|
|
|
if (stringBuffer.length > 0) {
|
|
tokens.push(stringBuffer);
|
|
}
|
|
|
|
return tokens;
|
|
}
|
|
shell.resolvePathInput = function(input) {
|
|
// replace revslashes into rslashes
|
|
var pathstr = input.replaceAll('\\','/');
|
|
var startsWithSlash = input.startsWith('/');
|
|
var newPwd = [];
|
|
|
|
// split them into an array while filtering empty elements except for the root 'head'
|
|
var ipwd = (startsWithSlash ? [""] : shell_pwd).concat(pathstr.split("/").filter(function(it) { return (it.length > 0); }));
|
|
|
|
serial.println("command.js > resolvePathInput > ipwd = "+ipwd);
|
|
serial.println("command.js > resolvePathInput > newPwd = "+newPwd);
|
|
|
|
// process dots
|
|
ipwd.forEach(function(it) {
|
|
serial.println("command.js > resolvePathInput > ipwd.forEach > it = "+it);
|
|
if (it === ".." && newPwd[1] !== undefined) {
|
|
newPwd.pop();
|
|
}
|
|
else if (it !== ".." && it !== ".") {
|
|
newPwd.push(it);
|
|
}
|
|
serial.println("command.js > resolvePathInput > newPwd = "+newPwd);
|
|
});
|
|
|
|
// construct new pathstr from pwd arr so it will be sanitised
|
|
pathstr = newPwd.join('/').substring(1);
|
|
|
|
return { string: pathstr, pwd: newPwd };
|
|
}
|
|
shell.coreutils = {
|
|
/* Args follow this format:
|
|
* <command-name> <1st arg> <2nd arg> ...
|
|
* NOTE:
|
|
* even if there's no 1st arg, length of args may not be 1, therefore don't:
|
|
* if (args.length < 2)
|
|
* but do instead:
|
|
* if (args[1] === undefined)
|
|
*/
|
|
cd: function(args) {
|
|
if (args[1] === undefined) {
|
|
println(CURRENT_DRIVE+":"+shell_pwd.join("/"));
|
|
return
|
|
}
|
|
var path = shell.resolvePathInput(args[1])
|
|
if (DEBUG_PRINT) serial.println("command.js > cd > pathstr = "+path.string);
|
|
|
|
// check if path is valid
|
|
var dirOpenedStatus = filesystem.open(CURRENT_DRIVE, path.string, 'R');
|
|
var isDir = filesystem.isDirectory(CURRENT_DRIVE); // open a dir; if path is nonexistent, file won't actually be opened
|
|
if (!isDir) { printerrln("디렉토리 '"+path.string+"'가 없습니다."); return dirOpenedStatus; } // if file is not opened, IO error code will be returned
|
|
|
|
shell_pwd = path.pwd;
|
|
},
|
|
mkdir: function(args) {
|
|
if (args[1] === undefined) {
|
|
printerrln("Syntax error");
|
|
return
|
|
}
|
|
var path = shell.resolvePathInput(args[1])
|
|
if (DEBUG_PRINT) serial.println("command.js > mkdir > pathstr = "+path.string);
|
|
|
|
// check if path is valid
|
|
var dirOpenedStatus = filesystem.open(CURRENT_DRIVE, path.string, 'W');
|
|
var mkdird = filesystem.mkDir(CURRENT_DRIVE);
|
|
if (!mkdird) { printerrln("디렉토리 생성 실패: '"+path.string+"'"); return dirOpenedStatus; }
|
|
},
|
|
cls: function(args) {
|
|
con.clear();
|
|
graphics.clearPixels(255);
|
|
},
|
|
exit: function(args) {
|
|
cmdExit = true;
|
|
},
|
|
ver: function(args) {
|
|
println(welcome_text);
|
|
},
|
|
echo: function(args) {
|
|
if (args[1] !== undefined) {
|
|
args.forEach(function(it,i) { if (i > 0) print(shell.replaceVarCall(it)+" ") });
|
|
}
|
|
println();
|
|
},
|
|
rem: function(args) {
|
|
return 0;
|
|
},
|
|
set: function(args) {
|
|
// print all the env vars
|
|
if (args[1] === undefined) {
|
|
Object.entries(_TVDOS.variables).forEach(function(a) { println(a[0]+"="+a[1]); })
|
|
}
|
|
else {
|
|
// parse key-value pair with splitter '='
|
|
var key = undefined; var value = undefined;
|
|
// if syntax "<key> = <value>" is used?
|
|
if ('=' == args[2]) {
|
|
key = args[1].toUpperCase(); value = args[3];
|
|
}
|
|
else if (args[2] === undefined) {
|
|
var pair = args[1].split('=');
|
|
key = pair[0].toUpperCase(); value = pair[1];
|
|
}
|
|
|
|
if (key == undefined) throw SyntaxError("Input format must be 'key=value'");
|
|
|
|
// if value is undefined, show what envvar[key] has
|
|
if (value === undefined) {
|
|
if (_TVDOS.variables[key] === undefined)
|
|
println("환경변수 '"+key+"'이(가) 없습니다.");
|
|
else
|
|
println(_TVDOS.variables[key])
|
|
}
|
|
else {
|
|
_TVDOS.variables[key] = shell.replaceVarCall(value);
|
|
|
|
// if key is KEYBOARD, reload the keyboard layout
|
|
if ("KEYBOARD" == key)
|
|
input.changeKeyLayout(_TVDOS.variables.KEYBOARD || "us_qwerty");
|
|
}
|
|
}
|
|
},
|
|
dir: function(args) {
|
|
var pathstr = (args[1] !== undefined) ? args[1] : shell.getPwdString();
|
|
|
|
// check if path is valid
|
|
var pathOpenedStatus = filesystem.open(CURRENT_DRIVE, pathstr, 'R');
|
|
if (pathOpenedStatus != 0) { printerrln("파일이 없습니다"); return pathOpenedStatus; }
|
|
|
|
var port = filesystem._toPorts(CURRENT_DRIVE)[0]
|
|
com.sendMessage(port, "LIST");
|
|
println(com.pullMessage(port));
|
|
},
|
|
cat: function(args) {
|
|
var pathstr = (args[1] !== undefined) ? args[1] : shell.getPwdString();
|
|
|
|
var pathOpenedStatus = filesystem.open(CURRENT_DRIVE, pathstr, 'R');
|
|
if (pathOpenedStatus != 0) { printerrln("파일이 없습니다"); return pathOpenedStatus; }
|
|
let contents = filesystem.readAll(CURRENT_DRIVE);
|
|
// TODO just print out what's there
|
|
print(contents);
|
|
}
|
|
};
|
|
shell.coreutils.chdir = shell.coreutils.cd;
|
|
Object.freeze(shell.coreutils);
|
|
shell.execute = function(line) {
|
|
if (0 == line.size) return;
|
|
var tokens = shell.parse(line);
|
|
var cmd = tokens[0];
|
|
if (cmd === undefined || cmd === '') return 0;
|
|
|
|
// handle Ctrl-C
|
|
if (con.hitterminate()) return 1;
|
|
|
|
if (shell.coreutils[cmd.toLowerCase()] !== undefined) {
|
|
var retval = shell.coreutils[cmd.toLowerCase()](tokens);
|
|
return retval|0; // return value of undefined will cast into 0
|
|
}
|
|
else {
|
|
// search through PATH for execution
|
|
|
|
var fileExists = false;
|
|
var searchDir = (cmd.startsWith("/")) ? [""] : ["/"+shell_pwd.join("/")].concat(_TVDOS.getPath());
|
|
|
|
var pathExt = []; // it seems Nashorn does not like 'let' too much? this line gets ignored sometimes
|
|
// fill pathExt using %PATHEXT% but also capitalise them
|
|
if (cmd.split(".")[1] === undefined)
|
|
_TVDOS.variables.PATHEXT.split(';').forEach(function(it) { pathExt.push(it); pathExt.push(it.toUpperCase()); });
|
|
else
|
|
pathExt.push(""); // final empty extension
|
|
|
|
searchLoop:
|
|
for (var i = 0; i < searchDir.length; i++) {
|
|
for (var j = 0; j < pathExt.length; j++) {
|
|
var search = searchDir[i]; if (!search.endsWith('\\')) search += '\\';
|
|
var path = trimStartRevSlash(search + cmd + pathExt[j]);
|
|
|
|
if (DEBUG_PRINT) {
|
|
serial.println("[command.js > shell.execute] file search path: "+path);
|
|
}
|
|
|
|
if (0 == filesystem.open(CURRENT_DRIVE, path, "R")) {
|
|
fileExists = true;
|
|
break searchLoop;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!fileExists) {
|
|
printerrln('명령어 또는 파일 이름이 틀립니다: "'+cmd+'"');
|
|
return 127;
|
|
}
|
|
else {
|
|
var programCode = filesystem.readAll(CURRENT_DRIVE);
|
|
var extension = undefined;
|
|
// get proper extension
|
|
var dotSepTokens = cmd.split('.');
|
|
if (dotSepTokens.length > 1) extension = dotSepTokens[dotSepTokens.length - 1].toUpperCase();
|
|
|
|
if ("BAT" == extension) {
|
|
// parse and run as batch file
|
|
var lines = programCode.split('\n').filter(function(it) { return it.length > 0; });
|
|
lines.forEach(function(line) {
|
|
shell.execute(line);
|
|
});
|
|
}
|
|
else {
|
|
let gotError = false;
|
|
|
|
try {
|
|
errorlevel = 0; // reset the number
|
|
|
|
if (_G.shellProgramTitles === undefined) _G.shellProgramTitles = [];
|
|
_G.shellProgramTitles.push(cmd.toUpperCase())
|
|
sendLcdMsg(_G.shellProgramTitles[_G.shellProgramTitles.length - 1]);
|
|
//serial.println(_G.shellProgramTitles);
|
|
|
|
errorlevel = execApp(programCode, tokens); // return value of undefined will cast into 0
|
|
}
|
|
catch (e) {
|
|
gotError = true;
|
|
|
|
serial.printerr(`[command.js] program quit with ${e}:\n${e.stack || '(stack trace unavailable)'}`);
|
|
|
|
if (`${e}`.startsWith("InterruptedException"))
|
|
errorlevel = SIGTERM.name;
|
|
else if (e instanceof IllegalAccessException || `${e}`.startsWith("net.torvald.tsvm.ErrorIllegalAccess"))
|
|
errorlevel = SIGSEGV.name;
|
|
|
|
// exception catched means something went wrong, so if errorlevel is found to be zero, force set to 1.
|
|
if (errorlevel === 0 || errorlevel == undefined)
|
|
errorlevel = 1;
|
|
}
|
|
finally {
|
|
// sometimes no-error program may return nothing as the errorlevel; force set to 0 then.
|
|
if (!gotError && (errorlevel == undefined || (typeof errorlevel.trim == "function" && errorlevel.trim().length == 0) || isNaN(errorlevel)))
|
|
errorlevel = 0;
|
|
|
|
serial.printerr(`errorlevel: ${errorlevel}`);
|
|
|
|
_G.shellProgramTitles.pop();
|
|
sendLcdMsg(_G.shellProgramTitles[_G.shellProgramTitles.length - 1]);
|
|
//serial.println(_G.shellProgramTitles);
|
|
|
|
return errorlevel;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
Object.freeze(shell);
|
|
_G.shell = shell;
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
if (exec_args[1] !== undefined) {
|
|
// only meaningful switches would be either -c or -k anyway
|
|
var firstSwitch = exec_args[1].toLowerCase();
|
|
|
|
// command -c <commands>
|
|
// ^[0] ^[1] ^[2]
|
|
if ("-c" == firstSwitch) {
|
|
if ("" == exec_args[2]) return 0; // no commands were given, just exit successfully
|
|
return shell.execute(exec_args[2]);
|
|
}
|
|
else if ("-k" == firstSwitch) {
|
|
if ("" == exec_args[2]) return 0; // no commands were given, just exit successfully
|
|
shell.execute(exec_args[2]);
|
|
goInteractive = true;
|
|
}
|
|
else if ("-fancy" == firstSwitch) {
|
|
graphics.setBackground(2,3,4);
|
|
goFancy = true;
|
|
goInteractive = true;
|
|
}
|
|
else {
|
|
printerrln("잘못된 스위치: "+exec_args[1]);
|
|
return 1;
|
|
}
|
|
}
|
|
else {
|
|
goInteractive = true;
|
|
}
|
|
|
|
let cmdExit = false;
|
|
if (goInteractive) {
|
|
con.curs_set(1);
|
|
greet();
|
|
|
|
let cmdHistory = []; // zeroth element is the oldest
|
|
let cmdHistoryScroll = 0; // 0 for outside-of-buffer, 1 for most recent
|
|
while (!cmdExit) {
|
|
con.curs_set(1);
|
|
print_prompt_text();
|
|
|
|
var cmdbuf = "";
|
|
|
|
while (true) {
|
|
let key = con.getch();
|
|
|
|
// printable chars
|
|
if (key >= 32 && key <= 126) {
|
|
var s = String.fromCharCode(key);
|
|
cmdbuf += s;
|
|
print(s);
|
|
}
|
|
// backspace
|
|
else if (key === con.KEY_BACKSPACE && cmdbuf.length > 0) {
|
|
cmdbuf = cmdbuf.substring(0, cmdbuf.length - 1);
|
|
print(String.fromCharCode(key));
|
|
}
|
|
// enter
|
|
else if (key === 10 || key === con.KEY_RETURN) {
|
|
println();
|
|
|
|
shell.execute(cmdbuf);
|
|
|
|
if (cmdbuf.trim().length > 0)
|
|
cmdHistory.push(cmdbuf);
|
|
|
|
cmdHistoryScroll = 0;
|
|
con.curs_set(1);
|
|
|
|
break;
|
|
}
|
|
// up arrow
|
|
else if (key === con.KEY_UP && cmdHistory.length > 0 && cmdHistoryScroll < cmdHistory.length) {
|
|
cmdHistoryScroll += 1;
|
|
|
|
// back the cursor in order to type new cmd
|
|
var x = 0;
|
|
for (x = 0; x < cmdbuf.length; x++) print(String.fromCharCode(8));
|
|
cmdbuf = cmdHistory[cmdHistory.length - cmdHistoryScroll];
|
|
// re-type the new command
|
|
print(cmdbuf);
|
|
|
|
}
|
|
// down arrow
|
|
else if (key === con.KEY_DOWN) {
|
|
if (cmdHistoryScroll > 0) {
|
|
// back the cursor in order to type new cmd
|
|
var x = 0;
|
|
for (x = 0; x < cmdbuf.length; x++) print(String.fromCharCode(8));
|
|
cmdbuf = cmdHistory[cmdHistory.length - cmdHistoryScroll];
|
|
// re-type the new command
|
|
print(cmdbuf);
|
|
|
|
cmdHistoryScroll -= 1;
|
|
}
|
|
else {
|
|
// back the cursor in order to type new cmd
|
|
var x = 0;
|
|
for (x = 0; x < cmdbuf.length; x++) print(String.fromCharCode(8));
|
|
cmdbuf = "";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0; |