· 6 years ago · Mar 13, 2020, 02:30 AM
1local args = { ... }
2
3local connections = {}
4
5local nshAPI = {
6 connList = connections
7}
8
9if not framebuffer then if not ((fs.exists("framebuffer") and os.loadAPI("framebuffer")) or (fs.exists("LyqydOS/framebuffer") and os.loadAPI("LyqydOS/framebuffer"))) then print("Couldn't find framebuffer API, using fallback") end end
10
11local function rawSend(id, msg)
12 if term.current then
13 return rednet.send(id, msg, "tror")
14 else
15 return rednet.send(id, msg)
16 end
17end
18
19local function rawRecv(id, timeout)
20 if type(timeout) == "number" then timeout = os.startTimer(timeout) end
21 while true do
22 event = {os.pullEvent()}
23 if event[1] == "rednet_message" and (id == nil and true or event[2] == id) and (not term.current and true or event[4] == "tror") then
24 return event[3]
25 elseif event[1] == "timer" and event[2] == timeout then
26 return nil
27 end
28 end
29end
30
31
32nshAPI.getRemoteID = function()
33 --check for connected clients with matching threads.
34 for cNum, cInfo in pairs(nshAPI.connList) do
35 if cInfo.thread == coroutine.running() then
36 if cNum == "localShell" then
37 --if we are a client running on the server, return the remote server ID.
38 if nshAPI.serverNum then
39 return nshAPI.serverNum
40 else
41 return nil
42 end
43 end
44 return cNum
45 end
46 end
47 --client running without local server, return remote server ID.
48 if nshAPI.serverNum then return nshAPI.serverNum end
49 return nil
50end
51
52nshAPI.send = function(msg)
53 local id = nshAPI.getRemoteID()
54 if id then
55 return rawSend(id, msg)
56 end
57 return nil
58end
59
60nshAPI.receive = function(timeout)
61 return rawRecv(nshAPI.getRemoteID(), timeout)
62end
63
64nshAPI.getClientCapabilities = function()
65 if nshAPI.clientCapabilities then return nshAPI.clientCapabilities end
66 nshAPI.send("SP:;clientCapabilities")
67 return nshAPI.receive(1)
68end
69
70nshAPI.getRemoteConnections = function()
71 local remotes = {}
72 for cNum, cInfo in pairs(nshAPI.connList) do
73 table.insert(remotes, cNum)
74 if cInfo.outbound then
75 table.insert(remotes, cInfo.outbound)
76 end
77 end
78 return remotes
79end
80
81nshAPI.packFile = function(path)
82 local data = {}
83 local count = 0
84 local handle = io.open(path, "rb")
85 if handle then
86 local byte = handle:read()
87 repeat
88 data[#data + 1] = byte
89 count = count + 1
90 if count % 1000 == 0 then
91 os.queueEvent("yield")
92 os.pullEvent("yield")
93 end
94 byte = handle:read()
95 until not byte
96 handle:close()
97 else
98 return false
99 end
100 local outputTable = {}
101 for i = 1, #data, 3 do
102 local num1, num2, num3 = data[i], data[i + 1] or 0, data[i + 2] or 0
103 table.insert(outputTable, string.char(bit32.band(bit32.arshift(num1, 2), 63)))
104 table.insert(outputTable, string.char(bit32.bor(bit32.band(bit32.lshift(num1, 4), 48), bit32.band(bit32.arshift(num2, 4), 15))))
105 table.insert(outputTable, string.char(bit32.bor(bit32.band(bit32.lshift(num2, 2), 60), bit32.band(bit32.arshift(num3, 6), 3))))
106 table.insert(outputTable, string.char(bit32.band(num3, 63)))
107 end
108 --mark non-data (invalid) bytes
109 if #data % 3 == 1 then
110 outputTable[#outputTable] = "="
111 outputTable[#outputTable - 1] = "="
112 elseif #data % 3 == 2 then
113 outputTable[#outputTable] = "="
114 end
115 return table.concat(outputTable, "")
116end
117
118nshAPI.unpackAndSaveFile = function(path, data)
119 local outputTable = {}
120 for i=1, #data, 4 do
121 local char1, char2, char3, char4 = string.byte(string.sub(data, i, i)), string.byte(string.sub(data, i + 1, i + 1)), string.byte(string.sub(data, i + 2, i + 2)), string.byte(string.sub(data, i + 3, i + 3))
122 table.insert(outputTable, bit32.band(bit32.bor(bit32.lshift(char1, 2), bit32.arshift(char2, 4)), 255))
123 table.insert(outputTable, bit32.band(bit32.bor(bit32.lshift(char2, 4), bit32.arshift(char3, 2)), 255))
124 table.insert(outputTable, bit32.band(bit32.bor(bit32.lshift(char3, 6), char4), 255))
125 end
126 --clean invalid bytes if marked
127 if string.sub(data, #data, #data) == "=" then
128 table.remove(outputTable)
129 if string.sub(data, #data - 1, #data - 1) == "=" then
130 table.remove(outputTable)
131 end
132 end
133 local handle = io.open(path, "wb")
134 if handle then
135 for i = 1, #outputTable do
136 handle:write(outputTable[i])
137 if i % 10 == 0 then
138 os.startTimer(0.1)
139 os.pullEvent("timer")
140 end
141 end
142 handle:close()
143 end
144end
145
146local packetConversion = {
147 query = "SQ",
148 response = "SR",
149 data = "SP",
150 close = "SC",
151 fileQuery = "FQ",
152 fileSend = "FS",
153 fileResponse = "FR",
154 fileHeader = "FH",
155 fileData = "FD",
156 fileEnd = "FE",
157 textWrite = "TW",
158 textCursorPos = "TC",
159 textGetCursorPos = "TG",
160 textGetSize = "TD",
161 textInfo = "TI",
162 textClear = "TE",
163 textClearLine = "TL",
164 textScroll = "TS",
165 textBlink = "TB",
166 textColor = "TF",
167 textBackground = "TK",
168 textIsColor = "TA",
169 textTable = "TT",
170 event = "EV",
171 SQ = "query",
172 SR = "response",
173 SP = "data",
174 SC = "close",
175 FQ = "fileQuery",
176 FS = "fileSend",
177 FR = "fileResponse",
178 FH = "fileHeader",
179 FD = "fileData",
180 FE = "fileEnd",
181 TW = "textWrite",
182 TC = "textCursorPos",
183 TG = "textGetCursorPos",
184 TD = "textGetSize",
185 TI = "textInfo",
186 TE = "textClear",
187 TL = "textClearLine",
188 TS = "textScroll",
189 TB = "textBlink",
190 TF = "textColor",
191 TK = "textBackground",
192 TA = "textIsColor",
193 TT = "textTable",
194 EV = "event",
195}
196
197local function openModem()
198 local modemFound = false
199 for _, side in ipairs(rs.getSides()) do
200 if peripheral.getType(side) == "modem" then
201 if not rednet.isOpen(side) then rednet.open(side) end
202 modemFound = true
203 break
204 end
205 end
206 return modemFound
207end
208
209local function send(id, pType, message)
210 if pType and message then
211 return rawSend(id, packetConversion[pType]..":;"..message)
212 end
213end
214
215local function awaitResponse(id, time)
216 id = tonumber(id)
217 local listenTimeOut = nil
218 local messRecv = false
219 if time then listenTimeOut = os.startTimer(time) end
220 while not messRecv do
221 local event, p1, p2 = os.pullEvent()
222 if event == "timer" and p1 == listenTimeOut then
223 return false
224 elseif event == "rednet_message" then
225 sender, message = p1, p2
226 if id == sender and message then
227 if packetConversion[string.sub(message, 1, 2)] then packetType = packetConversion[string.sub(message, 1, 2)] end
228 message = string.match(message, ";(.*)")
229 messRecv = true
230 end
231 end
232 end
233 return packetType, message
234end
235
236local function processText(conn, pType, value)
237 if not pType then return false end
238 if pType == "textWrite" and value then
239 term.write(value)
240 elseif pType == "textClear" then
241 term.clear()
242 elseif pType == "textClearLine" then
243 term.clearLine()
244 elseif pType == "textGetCursorPos" then
245 local x, y = term.getCursorPos()
246 send(conn, "textInfo", math.floor(x)..","..math.floor(y))
247 elseif pType == "textCursorPos" then
248 local x, y = string.match(value, "(%-?%d+),(%-?%d+)")
249 term.setCursorPos(tonumber(x), tonumber(y))
250 elseif pType == "textBlink" then
251 if value == "true" then
252 term.setCursorBlink(true)
253 else
254 term.setCursorBlink(false)
255 end
256 elseif pType == "textGetSize" then
257 x, y = term.getSize()
258 send(conn, "textInfo", x..","..y)
259 elseif pType == "textScroll" and value then
260 term.scroll(tonumber(value))
261 elseif pType == "textIsColor" then
262 send(conn, "textInfo", tostring(term.isColor()))
263 elseif pType == "textColor" and value then
264 value = tonumber(value)
265 if (value == 1 or value == 32768) or term.isColor() then
266 term.setTextColor(value)
267 end
268 elseif pType == "textBackground" and value then
269 value = tonumber(value)
270 if (value == 1 or value == 32768) or term.isColor() then
271 term.setBackgroundColor(value)
272 end
273 elseif pType == "textTable" then
274 local linesTable = textutils.unserialize(value)
275 for i=1, linesTable.sizeY do
276 term.setCursorPos(1,i)
277 local lineEnd = false
278 local offset = 1
279 while not lineEnd do
280 local textColorString = string.match(string.sub(linesTable.textColor[i], offset), string.sub(linesTable.textColor[i], offset, offset).."*")
281 local backColorString = string.match(string.sub(linesTable.backColor[i], offset), string.sub(linesTable.backColor[i], offset, offset).."*")
282 term.setTextColor(2 ^ tonumber(string.sub(textColorString, 1, 1), 16))
283 term.setBackgroundColor(2 ^ tonumber(string.sub(backColorString, 1, 1), 16))
284 term.write(string.sub(linesTable.text[i], offset, offset + math.min(#textColorString, #backColorString) - 1))
285 offset = offset + math.min(#textColorString, #backColorString)
286 if offset > linesTable.sizeX then lineEnd = true end
287 end
288 end
289 term.setCursorPos(linesTable.cursorX, linesTable.cursorY)
290 term.setCursorBlink(linesTable.cursorBlink)
291 end
292 return
293end
294
295local function textRedirect(id)
296 local textTable = {}
297 textTable.id = id
298 textTable.write = function(text)
299 return send(textTable.id, "textWrite", text)
300 end
301 textTable.clear = function()
302 return send(textTable.id, "textClear", "nil")
303 end
304 textTable.clearLine = function()
305 return send(textTable.id, "textClearLine", "nil")
306 end
307 textTable.getCursorPos = function()
308 send(textTable.id, "textGetCursorPos", "nil")
309 local pType, message = awaitResponse(textTable.id, 2)
310 if pType and pType == "textInfo" then
311 local x, y = string.match(message, "(%-?%d+),(%-?%d+)")
312 return tonumber(x), tonumber(y)
313 end
314 end
315 textTable.setCursorPos = function(x, y)
316 return send(textTable.id, "textCursorPos", math.floor(x)..","..math.floor(y))
317 end
318 textTable.setCursorBlink = function(b)
319 if b then
320 return send(textTable.id, "textBlink", "true")
321 else
322 return send(textTable.id, "textBlink", "false")
323 end
324 end
325 textTable.getSize = function()
326 send(textTable.id, "textGetSize", "nil")
327 local pType, message = awaitResponse(textTable.id, 2)
328 if pType and pType == "textInfo" then
329 local x, y = string.match(message, "(%d+),(%d+)")
330 return tonumber(x), tonumber(y)
331 end
332 end
333 textTable.scroll = function(lines)
334 return send(textTable.id, "textScroll", lines)
335 end
336 textTable.isColor = function()
337 send(textTable.id, "textIsColor", "nil")
338 local pType, message = awaitResponse(textTable.id, 2)
339 if pType and pType == "textInfo" then
340 if message == "true" then
341 return true
342 end
343 end
344 return false
345 end
346 textTable.isColour = textTable.isColor
347 textTable.setTextColor = function(color)
348 return send(textTable.id, "textColor", tostring(color))
349 end
350 textTable.setTextColour = textTable.setTextColor
351 textTable.setBackgroundColor = function(color)
352 return send(textTable.id, "textBackground", tostring(color))
353 end
354 textTable.setBackgroundColour = textTable.setBackgroundColor
355 return textTable
356end
357
358local function getServerID(server)
359 if tonumber(server) then
360 return tonumber(server)
361 elseif term.current then
362 return rednet.lookup("tror", args[1])
363 end
364end
365
366local function resumeThread(conn, event)
367 local cInfo = connections[conn]
368 if not connections[conn].filter or event[1] == connections[conn].filter then
369 connections[conn].filter = nil
370 local _oldTerm = term.redirect(connections[conn].target)
371 local passback = {coroutine.resume(connections[conn].thread, table.unpack(event))}
372 if passback[1] and passback[2] then
373 connections[conn].filter = passback[2]
374 end
375 if coroutine.status(connections[conn].thread) == "dead" then
376 send(conn, "close", "disconnect")
377 connections[conn] = nil
378 end
379 if _oldTerm then
380 term.redirect(_oldTerm)
381 else
382 term.restore()
383 end
384 if connections[conn] and conn ~= "localShell" and framebuffer then
385 send(conn, "textTable", textutils.serialize(connections[conn].target.buffer))
386 end
387 end
388end
389
390local eventFilter = {
391 key = true,
392 char = true,
393 mouse_click = true,
394 mouse_drag = true,
395 mouse_scroll = true,
396}
397
398local function newSession(conn, x, y, color)
399 local session = {}
400 local path = "/rom/programs/shell"
401 if #args >= 2 and shell.resolveProgram(args[2]) then path = shell.resolveProgram(args[2]) end
402 session.thread = coroutine.create(function() shell.run(path) end)
403 if framebuffer then
404 session.target = framebuffer.new(x, y, color)
405 else
406 session.target = textRedirect(conn)
407 end
408 session.status = "open"
409 _oldTerm = term.redirect(session.target)
410 coroutine.resume(session.thread)
411 if _oldTerm then
412 term.redirect(_oldTerm)
413 else
414 term.restore()
415 end
416 if framebuffer then
417 send(conn, "textTable", textutils.serialize(session.target.buffer))
418 end
419 return session
420end
421
422if #args >= 1 and args[1] == "host" then
423 _G.nsh = nshAPI
424 if not openModem() then return end
425 if term.current then
426 if args[4] then
427 rednet.host("tror", args[4])
428 elseif os.getComputerLabel() then
429 rednet.host("tror", os.getComputerLabel())
430 else
431 print("No label or hostname provided!")
432 return
433 end
434 end
435 local connInfo = {}
436 connInfo.target = term.current and term.current() or term.native
437 local path = "/rom/programs/shell"
438 if #args >= 3 and shell.resolveProgram(args[3]) then path = shell.resolveProgram(args[3]) end
439 connInfo.thread = coroutine.create(function() shell.run(path) end)
440 connections.localShell = connInfo
441 term.clear()
442 term.setCursorPos(1,1)
443 coroutine.resume(connections.localShell.thread)
444
445 while true do
446 event = {os.pullEventRaw()}
447 if event[1] == "rednet_message" then
448 if type(event[3]) == "string" and packetConversion[string.sub(event[3], 1, 2)] then
449 --this is a packet meant for us.
450 conn = event[2]
451 packetType = packetConversion[string.sub(event[3], 1, 2)]
452 message = string.match(event[3], ";(.*)")
453 if connections[conn] and connections[conn].status == "open" then
454 if packetType == "event" or string.sub(packetType, 1, 4) == "text" then
455 local eventTable = {}
456 if packetType == "event" then
457 eventTable = textutils.unserialize(message)
458 else
459 --we can pass the packet in raw, since this is not an event packet.
460 eventTable = event
461 end
462 resumeThread(conn, eventTable)
463 elseif packetType == "query" then
464 local connType, color, x, y = string.match(message, "(%a+):(%a+);(%d+),(%d+)")
465 if connType == "connect" or (connType == "resume" and (not framebuffer)) then
466 --reset connection
467 send(conn, "response", "OK")
468 connections[conn] = newSession(conn, tonumber(x), tonumber(y), color == "true")
469 elseif connType == "resume" then
470 --restore connection
471 send(conn, "response", "OK")
472 send(conn, "textTable", textutils.serialize(connections[conn].target.buffer))
473 end
474 elseif packetType == "close" then
475 connections[conn] = nil
476 send(conn, "close", "disconnect")
477 --close connection
478 else
479 --we got a packet, have an open connection, but despite it being in the conversion table, don't handle it ourselves. Send it onward.
480 resumeThread(conn, event)
481 end
482 elseif packetType ~= "query" then
483 --usually, we would send a disconnect here, but this prevents one from hosting nsh and connecting to other computers. Pass these to all shells as well.
484 for cNum, cInfo in pairs(connections) do
485 resumeThread(cNum, event)
486 end
487 else
488 --open new connection
489 send(conn, "response", "OK")
490 local color, x, y = string.match(message, "connect:(%a+);(%d+),(%d+)")
491 local connInfo = newSession(conn, tonumber(x), tonumber(y), color == "true")
492 connections[conn] = connInfo
493 end
494 else
495 --rednet message, but not in the correct format, so pass to all shells.
496 for cNum, cInfo in pairs(connections) do
497 resumeThread(cNum, event)
498 end
499 end
500 elseif eventFilter[event[1]] then
501 --user interaction.
502 coroutine.resume(connections.localShell.thread, table.unpack(event))
503 if coroutine.status(connections.localShell.thread) == "dead" then
504 for cNum, cInfo in pairs(connections) do
505 if cNum ~= "localShell" then
506 send(cNum, "close", "disconnect")
507 end
508 end
509 return
510 end
511 else
512 --dispatch all other events to all shells
513 for cNum, cInfo in pairs(connections) do
514 resumeThread(cNum, event)
515 end
516 end
517 end
518
519elseif #args <= 2 and nsh and nsh.getRemoteID() then
520 print(nsh.getRemoteID())
521 --forwarding mode
522 local conns = nsh.getRemoteConnections()
523 for i = 1, #conns do
524 if conns[i] == serverNum then
525 print("Cyclic connection refused.")
526 return
527 end
528 end
529 local fileTransferState = nil
530 local fileData = nil
531 local serverNum = getServerID(args[1])
532 if not serverNum then
533 print("Server Not Found")
534 return
535 end
536 send(serverNum, "query", "connect")
537 local pType, message = awaitResponse(serverNum, 2)
538 if pType ~= "response" then
539 print("Connection Failed")
540 return
541 else
542 nsh.connList[nsh.getRemoteID()].outbound = serverNum
543 term.clear()
544 term.setCursorPos(1,1)
545 end
546 local clientID = nsh.getRemoteID()
547 local serverID = tonumber(args[1])
548 while true do
549 event = {os.pullEvent()}
550 if event[1] == "rednet_message" then
551 if event[2] == clientID or event[2] == serverID then
552 if event[2] == serverID and string.sub(event[3], 1, 2) == "SC" then break end
553 rednet.send((event[2] == clientID and serverID or clientID), event[3])
554 end
555 elseif eventFilter[event[1]] then
556 rednet.send(serverID, "EV:;"..textutils.serialize(event))
557 end
558 end
559 nsh.connList[nsh.getRemoteID()].outbound = nil
560 term.clear()
561 term.setCursorPos(1, 1)
562 print("Connection closed by server")
563
564elseif #args >= 1 then --either no server running or we are the local shell on the server.
565 if not openModem() then return end
566 local serverNum = getServerID(args[1])
567 if not serverNum then
568 print("Server Not Found")
569 return
570 end
571 if nsh then
572 local conns = nsh.getRemoteConnections()
573 for i = 1, #conns do
574 if conns[i] == serverNum then
575 print("Connection refused.")
576 return
577 end
578 end
579 end
580 local fileTransferState = nil
581 local fileData = nil
582 local fileBinaryData = nil
583 local unpackCo = {}
584 local color = term.isColor()
585 local x, y = term.getSize()
586 if args[2] == "resume" then
587 send(serverNum, "query", "resume:"..tostring(color)..";"..tostring(x)..","..tostring(y))
588 else
589 send(serverNum, "query", "connect:"..tostring(color)..";"..tostring(x)..","..tostring(y))
590 end
591 local timeout = os.startTimer(2)
592 while true do
593 local event = {os.pullEvent()}
594 if event[1] == "timer" and event[2] == timeout then
595 print("Connection failed.")
596 return
597 elseif event[1] == "rednet_message" and event[2] == serverNum and string.sub(event[3], 1, 2) == "SR" then
598 if nsh then nshAPI = nsh end
599 if nshAPI.connList and nshAPI.connList.localShell then nshAPI.connList.localShell.outbound = serverNum end
600 nshAPI.serverNum = serverNum
601 nshAPI.clientCapabilities = "-fileTransfer-extensions-"
602 term.clear()
603 term.setCursorPos(1,1)
604 break
605 end
606 end
607
608 while true do
609 event = {os.pullEventRaw()}
610 if #unpackCo > 0 then
611 for i = #unpackCo, 1, -1 do
612 if coroutine.status(unpackCo[i]) ~= "dead" then
613 coroutine.resume(unpackCo[i], table.unpack(event))
614 else
615 table.remove(unpackCo, i)
616 end
617 end
618 end
619 if event[1] == "rednet_message" and event[2] == serverNum then
620 if packetConversion[string.sub(event[3], 1, 2)] then
621 packetType = packetConversion[string.sub(event[3], 1, 2)]
622 message = string.match(event[3], ";(.*)")
623 if string.sub(packetType, 1, 4) == "text" then
624 processText(serverNum, packetType, message)
625 elseif packetType == "data" then
626 if message == "clientCapabilities" then
627 rednet.send(serverNum, nshAPI.clientCapabilities)
628 end
629 elseif packetType == "fileQuery" then
630 --send a file to the server
631 local mode, file = string.match(message, "^(%a)=(.*)")
632 if fs.exists(file) then
633 send(serverNum, "fileHeader", file)
634 if mode == "b" then
635 local fileString = nshAPI.packFile(file)
636 send(serverNum, "fileData", "b="..fileString)
637 else
638 local handle = io.open(file, "r")
639 if handle then
640 send(serverNum, "fileData", "t="..handle:read("*a"))
641 handle:close()
642 end
643 end
644 else
645 send(serverNum, "fileHeader", "fileNotFound")
646 end
647 send(serverNum, "fileEnd", "end")
648 elseif packetType == "fileSend" then
649 --receive a file from the server, but don't overwrite existing files.
650 local mode, file = string.match(message, "^(%a)=(.*)")
651 if not fs.exists(file) then
652 fileTransferState = "receive_wait:"..file
653 send(serverNum, "fileResponse", "ok")
654 if mode == "b" then
655 fileBinaryData = ""
656 fileData = nil
657 else
658 fileData = ""
659 fileBinaryData = nil
660 end
661 else
662 send(serverNum, "fileResponse", "reject")
663 end
664 elseif packetType == "fileHeader" then
665 if message == "fileNotFound" then
666 fileTransferState = nil
667 end
668 elseif packetType == "fileData" then
669 if fileTransferState and string.match(fileTransferState, "(.-):") == "receive_wait" then
670 if string.match(message, "^(%a)=") == "b" then
671 fileBinaryData = fileBinaryData..string.match(message, "^b=(.*)")
672 else
673 fileData = fileData..string.match(message, "^t=(.*)")
674 end
675 end
676 elseif packetType == "fileEnd" then
677 if fileTransferState and string.match(fileTransferState, "(.-):") == "receive_wait" then
678 if fileBinaryData then
679 local co = coroutine.create(nshAPI.unpackAndSaveFile)
680 coroutine.resume(co, string.match(fileTransferState, ":(.*)"), fileBinaryData)
681 if coroutine.status(co) ~= "dead" then
682 table.insert(unpackCo, co)
683 end
684 elseif fileData then
685 local handle = io.open(string.match(fileTransferState, ":(.*)"), "w")
686 if handle then
687 handle:write(fileData)
688 handle:close()
689 end
690 end
691 fileTransferState = nil
692 end
693 elseif packetType == "close" then
694 if term.isColor() then
695 term.setBackgroundColor(colors.black)
696 term.setTextColor(colors.white)
697 end
698 term.clear()
699 term.setCursorPos(1, 1)
700 print("Connection closed by server.")
701 nshAPI.serverNum = nil
702 if nshAPI.connList and nshAPI.connList.localShell then nshAPI.connList.localShell.outbound = nil end
703 return
704 end
705 end
706 elseif event[1] == "mouse_click" or event[1] == "mouse_drag" or event[1] == "mouse_scroll" or event[1] == "key" or event[1] == "char" then
707 --pack up event
708 send(serverNum, "event", textutils.serialize(event))
709 elseif event[1] == "terminate" then
710 nshAPI.serverNum = nil
711 if nshAPI.localShell then nshAPI.localShell.outbound = nil end
712 term.clear()
713 term.setCursorPos(1, 1)
714 print("Connection closed locally.")
715 return
716 end
717 end
718else
719 print("Usage: nsh <serverID> [resume]")
720 print(" nsh host [remote [local [name]]]")
721end