diff --git a/.gitmodules b/.gitmodules index 371e0452..be99304b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -65,3 +65,9 @@ [submodule "libs/codetracer-python-recorder"] path = libs/codetracer-python-recorder url = https://github.com/metacraft-labs/codetracer-python-recorder.git +[submodule "libs/nimcrypto"] + path = libs/nimcrypto + url = https://github.com/cheatfate/nimcrypto.git +[submodule "libs/zip"] + path = libs/zip + url = git@github.com:nim-lang/zip.git diff --git a/libs/nimcrypto b/libs/nimcrypto new file mode 160000 index 00000000..69eec037 --- /dev/null +++ b/libs/nimcrypto @@ -0,0 +1 @@ +Subproject commit 69eec0375dd146aede41f920c702c531bfe89c6b diff --git a/libs/zip b/libs/zip new file mode 160000 index 00000000..06f5b0a0 --- /dev/null +++ b/libs/zip @@ -0,0 +1 @@ +Subproject commit 06f5b0a0767b14c7595ed168611782be69e61543 diff --git a/nim.cfg b/nim.cfg index 97e65ae6..224d88e6 100644 --- a/nim.cfg +++ b/nim.cfg @@ -23,5 +23,7 @@ path:"libs/chronos" path:"libs/parsetoml/src" path:"libs/nim-result" path:"libs/nim-confutils" +path:"libs/nimcrypto" +path:"libs/zip" gcc.options.debug = "-O0 -g3" diff --git a/nix/shells/main.nix b/nix/shells/main.nix index cac7a7e1..725e5c4f 100644 --- a/nix/shells/main.nix +++ b/nix/shells/main.nix @@ -84,6 +84,7 @@ in unixtools.killall # zip # unzip + libzip # curl # for pgrep at least @@ -150,7 +151,7 @@ in # copied case for libstdc++.so (needed by better-sqlite3) from # https://discourse.nixos.org/t/what-package-provides-libstdc-so-6/18707/4: # gcc.cc.lib .. - export CT_LD_LIBRARY_PATH="${sqlite.out}/lib/:${pcre.out}/lib:${glib.out}/lib:${openssl.out}/lib:${gcc.cc.lib}/lib"; + export CT_LD_LIBRARY_PATH="${sqlite.out}/lib/:${pcre.out}/lib:${glib.out}/lib:${openssl.out}/lib:${gcc.cc.lib}/lib:${libzip.out}/lib"; export RUST_LOG=info diff --git a/src/common/common_trace_index.nim b/src/common/common_trace_index.nim index b5ad1901..c34a253f 100644 --- a/src/common/common_trace_index.nim +++ b/src/common/common_trace_index.nim @@ -55,7 +55,8 @@ const SQL_INITIAL_INSERT_STATEMENTS = @[ const SQL_ALTER_TABLE_STATEMENTS: seq[string] = @[ # example: adding a new column - """ALTER TABLE traces ADD COLUMN calltraceMode text;""", - """ALTER TABLE traces RENAME COLUMN callgraph TO calltrace""" + """ALTER TABLE traces ADD COLUMN remoteShareDownloadId text;""", + """ALTER TABLE traces ADD COLUMN remoteShareControlId text;""", + """ALTER TABLE traces ADD COLUMN remoteShareExpireTime INTEGER DEFAULT -1;""" # """ALTER TABLE traces ADD COLUMN love integer;""" ] diff --git a/src/common/common_types.nim b/src/common/common_types.nim index 0640b889..d8d2f71e 100644 --- a/src/common/common_types.nim +++ b/src/common/common_types.nim @@ -153,6 +153,9 @@ type rrPid*: int exitCode*: int calltraceMode*: CalltraceMode + downloadKey*: langstring + controlId*: langstring + onlineExpireTime*: int CalltraceMode* {.pure.} = enum NoInstrumentation, CallKeyOnly, RawRecordNoValues, FullRecord @@ -1341,6 +1344,19 @@ type BugReportArg* = object ## BugReport arg title*: langstring description*: langstring + + UploadTraceArg* = object + trace*: Trace + programName*: langstring + + UploadedTraceData* = object + downloadKey*: langstring + controlId*: langstring + expireTime*: langstring + + DeleteTraceArg* = object + traceId*: int + controlId*: langstring DbEventKind* {.pure.} = enum Record, Trace, History diff --git a/src/common/trace_index.nim b/src/common/trace_index.nim index c8474e3b..c3e54329 100644 --- a/src/common/trace_index.nim +++ b/src/common/trace_index.nim @@ -64,6 +64,47 @@ proc ensureDB(test: bool): DBConn = globalDbMap[test.int] = db db +proc updateField*( + id: int, + fieldName: string, + fieldValue: string, + test: bool +) = + let db = ensureDB(test) + db.exec( + sql(&"UPDATE traces SET {fieldName} = ? WHERE id = ?"), + fieldValue, id + ) + db.close() + +proc updateField*( + id: int, + fieldName: string, + fieldValue: int, + test: bool +) = + let db = ensureDB(test) + db.exec( + sql(&"UPDATE traces SET {fieldName} = ? WHERE id = ?"), + fieldValue, id + ) + db.close() + +proc getField*( + id: int, + fieldName: string, + test: bool +): string = + let db = ensureDB(test) + let res = db.getAllRows( + sql(&"SELECT {fieldName} FROM traces WHERE id = ? LIMIT 1"), + id + ) + db.close() + if res.len > 0: + return res[0][0] + return "" + proc recordTrace*( id: int, program: string, @@ -81,7 +122,8 @@ proc recordTrace*( exitCode: int, calltrace: bool, calltraceMode: CalltraceMode, - test: bool): Trace = + test: bool, + downloadKey: string = ""): Trace = # TODO pass here a Trace value and instead if neeeded construct it from other helpers let currentDate: DateTime = now() @@ -107,19 +149,19 @@ proc recordTrace*( sourceFolders, lowLevelFolder, outputFolder, lang, imported, shellID, rrPid, exitCode, - calltrace, calltraceMode, date) + calltrace, calltraceMode, date, remoteShareDownloadId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, - ?, ?, ?)""", + ?, ?, ?, ?)""", $id, program, args.join(" "), compileCommand, env, workdir, "", # <- output sourceFolders, lowLevelFolder, outputFolder, $(lang.int), $(imported.int), $shellID, $rrPid, $exitCode, - ord(calltrace), $calltraceMode, $traceDate) + ord(calltrace), $calltraceMode, $traceDate, downloadKey) break except DbError: echo "error: ", getCurrentExceptionMsg() @@ -177,6 +219,12 @@ proc loadCalltraceMode*(raw: string, lang: Lang): CalltraceMode = proc loadTrace(trace: Row, test: bool): Trace = try: let lang = trace[10].parseInt.Lang + var expireTime = -1 + try: + expireTime = trace[20].parseInt + except: + discard + result = Trace( id: trace[0].parseInt, program: trace[1], @@ -196,7 +244,10 @@ proc loadTrace(trace: Row, test: bool): Trace = shellID: trace[14].parseInt, calltrace: trace[15].parseInt != 0, - calltraceMode: loadCalltraceMode(trace[16], lang)) + calltraceMode: loadCalltraceMode(trace[16], lang), + downloadKey: trace[18], + controlId: trace[19], + onlineExpireTime: expireTime) except CatchableError as e: # assume db schema change? echo "internal error: ", e.msg diff --git a/src/config/default_config.yaml b/src/config/default_config.yaml index f1c47253..3991e48b 100644 --- a/src/config/default_config.yaml +++ b/src/config/default_config.yaml @@ -54,7 +54,7 @@ defaultBuild: "" showMinimap: true # for now local setup -webApiRoot: http://100.87.206.30:57103/api/codetracer +webApiRoot: http://localhost:55500/api/codetracer/anon # # you can use KEY+OTHER # # use PageUp, PageDown, CTRL, ALT, SHIFT diff --git a/src/ct/codetracerconf.nim b/src/ct/codetracerconf.nim index 7fcfa0ff..7d302c21 100644 --- a/src/ct/codetracerconf.nim +++ b/src/ct/codetracerconf.nim @@ -14,6 +14,7 @@ type install, upload, download, + cmdDelete, # build, record, console, @@ -264,7 +265,16 @@ type of download: traceRegistryId* {. argument, - desc: "the trace registry unique id: # e.g. a.rb#5" + desc: "the trace registry unique id: //// e.g. noir//1234//asd" + .}: string + of cmdDelete: + traceId* {. + name: "trace-id" + desc: "trace trace unique id" + .}: int + controlId* {. + name: "control-id", + desc: "the trace control id to delete the online trace" .}: string of start_core: coreTraceArg* {. diff --git a/src/ct/launch/launch.nim b/src/ct/launch/launch.nim index 18e6f882..8a5b7910 100644 --- a/src/ct/launch/launch.nim +++ b/src/ct/launch/launch.nim @@ -2,6 +2,7 @@ import std/[strutils, strformat, osproc], ../../common/[ paths, types, intel_fix, install_utils, trace_index, start_utils ], ../utilities/[ git, env ], + ../online_sharing/trace_manager, ../cli/[ logging, list, help ], ../trace/[ replay, record, run, metadata ], ../codetracerconf, @@ -101,15 +102,15 @@ proc runInitial*(conf: CodetracerConf) = notSupportedCommand($conf.cmd) of StartupCommand.upload: # similar to replay/console - notSupportedCommand($conf.cmd) - # eventually enable? - # uploadCommand( - # conf.uploadLastTraceMatchingPattern, - # conf.uploadTraceId, - # conf.uploadTraceFolder, - # replayInteractive) + uploadCommand( + conf.uploadLastTraceMatchingPattern, + conf.uploadTraceId, + conf.uploadTraceFolder, + replayInteractive) of StartupCommand.download: - notSupportedCommand($conf.cmd) + downloadCommand(conf.traceRegistryId) + of StartupCommand.cmdDelete: + deleteTraceCommand(conf.traceId, conf.controlId) # eventually enable? # downloadCommand(conf.traceRegistryId) # of StartupCommand.build: diff --git a/src/ct/online_sharing/security_upload.nim b/src/ct/online_sharing/security_upload.nim new file mode 100644 index 00000000..01218c6b --- /dev/null +++ b/src/ct/online_sharing/security_upload.nim @@ -0,0 +1,68 @@ +import nimcrypto, zip/zipfiles, std/[ sequtils, strutils, strformat, os, osproc ] +import ../../common/[ config ] + +proc generateSecurePassword*(): string = + var key: array[32, byte] + discard randomBytes(key) + + result = key.mapIt(it.toHex(2)).join("") + return result + +proc pkcs7Pad*(data: seq[byte], blockSize: int): seq[byte] = + let padLen = blockSize - (data.len mod blockSize) + result = data & repeat(cast[byte](padLen), padLen) + +proc pkcs7Unpad*(data: seq[byte]): seq[byte] = + if data.len == 0: + raise newException(ValueError, "Data is empty, cannot unpad") + + let padLen = int64(data[^1]) # Convert last byte to int64 safely + if padLen <= 0 or padLen > data.len: + raise newException(ValueError, "Invalid padding") + + result = data[0 ..< data.len - padLen] + +func toBytes*(s: string): seq[byte] = + ## Convert a string to the corresponding byte sequence - since strings in + ## nim essentially are byte sequences without any particular encoding, this + ## simply copies the bytes without a null terminator + when nimvm: + var r = newSeq[byte](s.len) + for i, c in s: + r[i] = cast[byte](c) + r + else: + @(s.toOpenArrayByte(0, s.high)) + +proc encryptZip(zipFile, password: string) = + var iv: seq[byte] = password.toBytes()[0..15] + + var aes: CBC[aes256] + aes.init(password.toOpenArrayByte(0, len(password) - 1), iv) + + var zipData = readFile(zipFile).toBytes() + var paddedData = pkcs7Pad(zipData, 16) + var encrypted = newSeq[byte](paddedData.len) + + aes.encrypt(paddedData, encrypted.toOpenArray(0, len(encrypted) - 1)) + writeFile(zipFile & ".enc", encrypted) + +proc zipFileWithEncryption*(inputFile: string, outputZip: string, password: string) = + var zip: ZipArchive + if not zip.open(outputZip, fmWrite): + raise newException(IOError, "Failed to create zip file: " & outputZip) + + for file in walkDirRec(inputFile): + let relPath = file.relativePath(inputFile) + zip.addFile(relPath, file) + + zip.close() + encryptZip(outputZip, password) + removeFile(outputZip) + +proc uploadEncyptedZip*(file: string): (string, int) = + # TODO: Plug in http client instead of curl + let config = loadConfig(folder=getCurrentDir(), inTest=false) + let cmd = &"curl -s -X POST -F \"file=@{file}.enc\" {config.webApiRoot}/upload" + let (output, exitCode) = execCmdEx(cmd) + (output, exitCode) diff --git a/src/ct/online_sharing/trace_manager.nim b/src/ct/online_sharing/trace_manager.nim new file mode 100644 index 00000000..802422d0 --- /dev/null +++ b/src/ct/online_sharing/trace_manager.nim @@ -0,0 +1,100 @@ +import std/[ options, strutils, os, osproc, strformat, json ], ../trace/replay, ../codetracerconf, zip/zipfiles, nimcrypto +import ../../common/[ config, trace_index, lang, paths ] +import ../utilities/language_detection +import ../trace/[ storage_and_import, record ] +import security_upload + +proc uploadCommand*( + patternArg: Option[string], + traceIdArg: Option[int], + traceFolderArg: Option[string], + interactive: bool +) = + discard internalReplayOrUpload(patternArg, traceIdArg, traceFolderArg, interactive, command=StartupCommand.upload) + + +proc decryptZip(encryptedFile: string, password: string, outputFile: string) = + var encData = readFile(encryptedFile).toBytes() + if encData.len < 16: + raise newException(ValueError, "Invalid encrypted data (too short)") + + let iv = password.toBytes()[0 ..< 16] + let ciphertext = encData[16 .. ^1] + let key = password.toBytes() + + var aes: CBC[aes256] + aes.init(key, iv) + + var decrypted = newSeq[byte](encData.len) + aes.decrypt(encData, decrypted.toOpenArray(0, len(decrypted) - 1)) + + var depaddedData = pkcs7Unpad(decrypted) + writeFile(outputFile, depaddedData) + +proc unzipDecryptedFile(zipFile: string, outputDir: string): (string, int) = + var zip: ZipArchive + if not zip.open(zipFile, fmRead): + raise newException(IOError, "Failed to open decrypted ZIP: " & zipFile) + + let traceId = trace_index.newID(false) + let outPath = outputDir / "trace-" & $traceId + + createDir(outPath) + zip.extractAll(outPath) + + zip.close() + return (outPath, traceId) + +proc downloadCommand*(traceRegistryId: string) = + # We expect a traceRegistryId to have :: + let stringSplit = traceRegistryId.split("//") + if stringSplit.len() != 3: + quit(1) + else: + let downloadId = stringSplit[1] + let password = stringSplit[2] + let zipPath = codetracerTmpPath / "tmp.zip" + let config = loadConfig(folder=getCurrentDir(), inTest=false) + let localPath = codetracerTmpPath / "tmp.zip.enc" + # TODO: Plug in an http client + let cmd = &"curl -s -o {localPath} {config.webApiRoot}/download?DownloadId={downloadId}" + let (output, exitCode) = execCmdEx(cmd) + + decryptZip(localPath, password, zipPath) + + let (traceFolder, traceId) = unzipDecryptedFile(zipPath, codetracerTraceDir) + let tracePath = traceFolder / "trace.json" + let traceJson = parseJson(readFile(tracePath)) + let traceMetadataPath = traceFolder / "trace_metadata.json" + + var pathValue = "" + + for item in traceJson: + if item.hasKey("Path"): + pathValue = item["Path"].getStr("") + break + + let lang = detectLang(pathValue, LangUnknown) + discard importDbTrace(traceMetadataPath, traceId, lang, DB_SELF_CONTAINED_DEFAULT, traceRegistryId) + + removeFile(localPath) + removeFile(zipPath) + + echo traceId + + quit(exitCode) + +proc deleteAndResetFields(id: int, test: bool) = + updateField(id, "remoteShareDownloadId", "", test) + updateField(id, "remoteShareControlId", "", test) + updateField(id, "remoteShareExpireTime", -1, test) + +proc deleteTraceCommand*(id: int, controlId: string) = + let config = loadConfig(folder=getCurrentDir(), inTest=false) + let cmd = &"curl -s {config.webApiRoot}/delete?ControlId={controlId}" + let (output, exitCode) = execCmdEx(cmd) + + if exitCode == 0: + deleteAndResetFields(id, false) + + quit(exitCode) diff --git a/src/ct/trace/record.nim b/src/ct/trace/record.nim index 49c90a40..3df81bd9 100644 --- a/src/ct/trace/record.nim +++ b/src/ct/trace/record.nim @@ -54,7 +54,7 @@ proc recordSymbols(sourceDir: string, outputFolder: string, lang: Lang) = # it's still good to have an option/opt-out, so we leave that # as a flag in the internals, but not exposed to user yet # that's why for now it's hardcoded for db -const DB_SELF_CONTAINED_DEFAULT = true +const DB_SELF_CONTAINED_DEFAULT* = true # rr patches for ruby/other vm-s: not supported now, instead # in db backend support only direct traces diff --git a/src/ct/trace/replay.nim b/src/ct/trace/replay.nim index 1a23a76c..1474616a 100644 --- a/src/ct/trace/replay.nim +++ b/src/ct/trace/replay.nim @@ -6,7 +6,7 @@ import std/[options ], shell, run -proc internalReplayOrUpload( +proc internalReplayOrUpload*( patternArg: Option[string], traceIdArg: Option[int], traceFolderArg: Option[string], diff --git a/src/ct/trace/storage_and_import.nim b/src/ct/trace/storage_and_import.nim index 9c0164ca..42808919 100644 --- a/src/ct/trace/storage_and_import.nim +++ b/src/ct/trace/storage_and_import.nim @@ -2,6 +2,7 @@ import std/[os, json, strutils, strformat, sets, algorithm], ../../common/[trace_index, lang, types, paths], ../utilities/git, + ../online_sharing/security_upload, json_serialization proc storeTraceFiles(paths: seq[string], traceFolder: string, lang: Lang) = @@ -78,10 +79,12 @@ proc processSourceFoldersList*(folderSet: HashSet[string], programDir: string = proc importDbTrace*( - traceMetadataPath: string, - traceIdArg: int, - lang: Lang = LangNoir, - selfContained: bool = true): Trace = + traceMetadataPath: string, + traceIdArg: int, + lang: Lang = LangNoir, + selfContained: bool = true, + downloadKey: string = "" +): Trace = let rawTraceMetadata = readFile(traceMetadataPath) let untypedJson = parseJson(rawTraceMetadata) let program = untypedJson{"program"}.getStr() @@ -155,8 +158,32 @@ proc importDbTrace*( calltrace = true, # for now always use FullRecord for db-backend # and ignore possible env var override - calltraceMode = CalltraceMode.FullRecord) + calltraceMode = CalltraceMode.FullRecord, + downloadKey = downloadKey) proc uploadTrace*(trace: Trace) = - echo "error: uploading traces not supported currently!" - quit(1) + let outputZip = trace.outputFolder / "tmp.zip" + let aesKey = generateSecurePassword() + + zipFileWithEncryption(trace.outputFolder, outputZip, aesKey) + + let (output, exitCode) = uploadEncyptedZip(outputZip) + let jsonMessage = parseJson(output) + let downloadKey = trace.program & "//" & jsonMessage["DownloadId"].getStr("") & "//" & aesKey + + if jsonMessage["DownloadId"].getStr("") notin @["", "Errored"]: + + updateField(trace.id, "remoteShareDownloadId", downloadKey, false) + updateField(trace.id, "remoteShareControlId", jsonMessage["ControlId"].getStr(""), false) + updateField(trace.id, "remoteShareExpireTime", jsonMessage["Expires"].getInt(), false) + + echo downloadKey + echo jsonMessage["ControlId"].getStr("") + echo jsonMessage["Expires"].getInt() + + else: + echo downloadKey + + removeFile(outputZip & ".enc") + + quit(exitCode) diff --git a/src/frontend/index.nim b/src/frontend/index.nim index 4006a8b5..1d0ab10c 100644 --- a/src/frontend/index.nim +++ b/src/frontend/index.nim @@ -27,6 +27,8 @@ data.start = now() var close = false proc showOpenDialog(dialog: JsObject, browserWindow: JsObject, options: JsObject): Future[JsObject] {.importjs: "#.showOpenDialog(#,#)".} +proc loadExistingRecord(traceId: int) {.async.} +proc prepareForLoadingTrace(traceId: int, pid: int) {.async.} proc isCtInstalled: bool @@ -800,6 +802,71 @@ proc onSearchProgram(sender: js, query: cstring) {.async.} = proc onLoadStepLines(sender: js, response: LoadStepLinesArg) {.async.} = discard debugger.loadStepLines(response) +proc onUploadTraceFile(sender: js, response: UploadTraceArg) {.async.} = + let res = await readProcessOutput( + codetracerExe.cstring, + @[ + j"upload", + j"--trace-folder=" & response.trace.outputFolder + ] + ) + + if res.isOk: + let splitData = res.v.split("\n") + if splitData.len() == 4: + let uploadData = UploadedTraceData( + downloadKey: splitData[0], + controlId: splitData[1], + expireTime: splitData[2] + ) + mainWindow.webContents.send( + "CODETRACER::uploaded-trace-received", + js{ + "argId": j(response.trace.program & ":" & $response.trace.id), + "value": uploadData + } + ) + else: + let uploadData = UploadedTraceData( + downloadKey: splitData[0], + ) + mainWindow.webContents.send( + "CODETRACER::uploaded-trace-received", + js{ + "argId": j(response.trace.program & ":" & $response.trace.id), + "value": uploadData + } + ) + +proc onDownloadTraceFile(sender: js, response: jsobject(downloadKey = seq[cstring])) {.async.} = + let res = await readProcessOutput( + codetracerExe.cstring, + @[j"download"].concat(response.downloadKey) + ) + + if res.isOk: + let traceId = parseInt($res.v.trim()) + await prepareForLoadingTrace(traceId, nodeProcess.pid.to(int)) + await loadExistingRecord(traceId) + +proc onDeleteOnlineTraceFile(sender: js, response: DeleteTraceArg) {.async.} = + let res = await readProcessOutput( + codetracerExe.cstring, + @[ + j"cmdDelete", + j"--trace-id=" & $response.traceId, + j"--control-id=" & response.controlId + ] + ) + + mainWindow.webContents.send( + "CODETRACER::deleted-online-trace-received", + js{ + "argId": j($response.traceId & ":" & response.controlId), + "value": res.isOk + } + ) + proc onSendBugReportAndLogs(sender: js, response: BugReportArg) {.async.} = let process = await runProcess( codetracerExe.cstring, @@ -1298,6 +1365,11 @@ proc configureIpcMain = "show-in-debug-instance" "send-bug-report-and-logs" + # Upload/Download + "upload-trace-file" + "download-trace-file" + "delete-online-trace-file" + "restart" # "debug-gdb" diff --git a/src/frontend/renderer.nim b/src/frontend/renderer.nim index 95f83163..f2220e61 100644 --- a/src/frontend/renderer.nim +++ b/src/frontend/renderer.nim @@ -488,6 +488,12 @@ proc onContextStartHistory*(sender: js, response: jsobject(inState=bool, express proc onLoadParsedExprsReceived*(sender: js, response: jsobject(argId=cstring, value=JsAssoc[cstring, seq[FlowExpression]])) = jsAsFunction[proc(response: JsAssoc[cstring, seq[FlowExpression]]): void](data.network.futures["load-parsed-exprs"][response.argId])(response.value) +proc onUploadedTraceReceived*(sender: js, response: jsobject(argId=cstring, value=UploadedTraceData)) = + jsAsFunction[proc(response: UploadedTraceData): void](data.network.futures["upload-trace-file"][response.argId])(response.value) + +proc onDeletedOnlineTraceReceived*(sender: js, response: jsobject(argId=cstring, value=bool)) = + jsAsFunction[proc(response: bool): void](data.network.futures["delete-online-trace-file"][response.argId])(response.value) + # TODO: make some kind of dsl? # locals proc onLoadLocalsReceived*(sender: js, response: jsobject(argId=cstring, value=JsAssoc[cstring, Value])) = diff --git a/src/frontend/styles/components/welcome_screen.styl b/src/frontend/styles/components/welcome_screen.styl index bfdacf88..86475121 100644 --- a/src/frontend/styles/components/welcome_screen.styl +++ b/src/frontend/styles/components/welcome_screen.styl @@ -440,3 +440,15 @@ height: 30px font-family: "FiraCode" font-size: 20px + +.online-functionality-buttons + display: flex + + #delete-button + padding-left: 4px + + #copy-button + padding-left: 4px + + .expire-time-text + white-space: nowrap diff --git a/src/frontend/types.nim b/src/frontend/types.nim index 2251ce2f..ee6e9ec0 100644 --- a/src/frontend/types.nim +++ b/src/frontend/types.nim @@ -1094,6 +1094,8 @@ type kind*: RecordStatusKind errorMessage*: cstring + NewDownloadRecord* = ref object + args*: seq[cstring] NewTraceRecord* = ref object kit*: cstring @@ -1131,7 +1133,9 @@ type options*: seq[WelcomeScreenOption] welcomeScreen*: bool newRecordScreen*: bool + openOnlineTrace*: bool newRecord*: NewTraceRecord + newDownload*: NewDownloadRecord loading*: bool loadingTrace*: Trace diff --git a/src/frontend/ui/editor.nim b/src/frontend/ui/editor.nim index c5fcc141..8c2198ec 100644 --- a/src/frontend/ui/editor.nim +++ b/src/frontend/ui/editor.nim @@ -311,9 +311,7 @@ proc styleLines(self: EditorViewComponent, editor: MonacoEditor, lines: seq[Mona self.decorations.mapIt(it[0])) if not self.data.ui.welcomeScreen.isNil: - self.data.ui.welcomeScreen.loading = false - self.data.ui.welcomeScreen.welcomeScreen = false - self.data.ui.welcomeScreen.newRecordScreen = false + self.data.ui.welcomeScreen.resetView() proc lineActionClick(self: EditorViewComponent, tabInfo: TabInfo, line: js) = var element = line diff --git a/src/frontend/ui/welcome_screen.nim b/src/frontend/ui/welcome_screen.nim index 288334ad..ffcb6533 100644 --- a/src/frontend/ui/welcome_screen.nim +++ b/src/frontend/ui/welcome_screen.nim @@ -2,6 +2,47 @@ import ../ui_helpers, ../../ct/version, ui_imports, ../types +import std/times except now + +const PROGRAM_NAME_LIMIT = 45 +const NO_EXPIRE_TIME = -1 +const EMPTY_STRING = "" + +proc uploadTrace(self: WelcomeScreenComponent, trace: Trace) {.async.} = + var uploadedData = await self.data.asyncSend( + "upload-trace-file", + UploadTraceArg( + trace: trace, + programName: trace.program + ), + &"{trace.program}:{trace.id}", UploadedTraceData + ) + + if uploadedData.downloadKey != "Errored": + trace.downloadKey = uploadedData.downloadKey + trace.controlId = uploadedData.controlId + trace.onlineExpireTime = ($uploadedData.expireTime).parseInt() + else: + trace.downloadKey = uploadedData.downloadKey + + self.data.redraw() + +proc deleteUploadedTrace(self: WelcomeScreenComponent, trace: Trace) {.async.} = + var deleted = await self.data.asyncSend( + "delete-online-trace-file", + DeleteTraceArg( + traceId: trace.id, + controlId: trace.controlId + ), + &"{trace.id}:{trace.controlId}", bool + ) + + if deleted: + trace.controlId = EMPTY_STRING + trace.downloadKey = EMPTY_STRING + trace.onlineExpireTime = NO_EXPIRE_TIME + + self.data.redraw() proc recentProjectView(self: WelcomeScreenComponent, trace: Trace): VNode = buildHtml( @@ -14,7 +55,7 @@ proc recentProjectView(self: WelcomeScreenComponent, trace: Trace): VNode = self.data.ipc.send "CODETRACER::load-recent-trace", js{ traceId: trace.id } ) ): - let programLimitName = 45 + let programLimitName = PROGRAM_NAME_LIMIT let limitedProgramName = if trace.program.len > programLimitName: ".." & ($trace.program)[^programLimitName..^1] else: @@ -26,6 +67,39 @@ proc recentProjectView(self: WelcomeScreenComponent, trace: Trace): VNode = separateBar() span(class = "recent-trace-title-content"): text limitedProgramName # TODO: tippy + tdiv(class = "online-functionality-buttons"): + if trace.onlineExpireTime != NO_EXPIRE_TIME: + let dt = fromUnix(trace.onlineExpireTime) + let time = dt.format("dd MM yyyy") + span(class = "expire-time-text"): + text &"Expires on {time}" + if trace.downloadKey == "" and trace.onlineExpireTime == NO_EXPIRE_TIME: + tdiv(class = "recent-trace-buttons", id = "upload-button"): + span( + onclick = proc(ev: Event, tg: VNode) = + ev.stopPropagation() + discard self.uploadTrace(trace) + ): + text "upload" + if trace.controlId != EMPTY_STRING: + tdiv(class = "recent-trace-buttons", id = "delete-button"): + span( + onclick = proc(ev: Event, tg: VNode) = + ev.stopPropagation() + discard self.deleteUploadedTrace(trace) + ): + + text "delete" + if trace.downloadKey != EMPTY_STRING: + tdiv(class = "recent-trace-buttons", id = "copy-button"): + span( + onclick = proc(ev: Event, tg: VNode) = + ev.stopPropagation() + clipboardCopy(trace.downloadKey) + ): + + text "copy" + # tdiv(class = "recent-trace-info"): # tdiv(class = "recent-trace-date"): # text trace.date @@ -205,6 +279,48 @@ proc prepareArgs(self: WelcomeScreenComponent): seq[cstring] = return args.concat(self.newRecord.args) +proc onlineFormView(self: WelcomeScreenComponent): VNode = + buildHtml( + tdiv(class = "new-record-form") + ): + renderInputRow( + "args", + "Download ID with password", + "", + proc(ev: Event, tg: VNode) = discard, + proc(ev: Event, tg: VNode) = self.newDownload.args = ev.target.value.split(" "), + hasButton = false, + inputText = self.newDownload.args.join(j" ") + ) + tdiv(class = "new-record-form-row"): + button( + class = "cancel-button", + onclick = proc(ev: Event, tg: VNode) = + ev.preventDefault() + self.welcomeScreen = true + self.openOnlineTrace = false + self.newDownload = nil + ): + text "Back" + button( + class = "confirmation-button", + onclick = proc(ev: Event, tg: VNode) = + ev.preventDefault() + self.loading = true + # TODO: Implement progress bar + # self.newRecord.status.kind = InProgress + # let workDir = if self.newRecord.workDir.isNil or self.newRecord.workDir.len == 0: + # jsUndefined + # else: + # cast[JsObject](self.newRecord.workDir) + self.data.ipc.send( + "CODETRACER::download-trace-file", js{ + downloadKey: concat(self.newDownload.args), + } + ) + ): + text "Download" + proc newRecordFormView(self: WelcomeScreenComponent): VNode = buildHtml( tdiv(class = "new-record-form") @@ -338,6 +454,16 @@ proc newRecordView(self: WelcomeScreenComponent): VNode = text "Start Debugger" newRecordFormView(self) +proc onlineTraceView(self: WelcomeScreenComponent): VNode = + buildHtml( + tdiv(class = "new-record-screen") + ): + tdiv(class = "new-record-screen-content"): + tdiv(class = "welcome-logo") + tdiv(class = "new-record-title"): + text "Start Debugger" + onlineFormView(self) + proc loadInitialOptions(self: WelcomeScreenComponent) = self.options = @[ WelcomeScreenOption( @@ -372,8 +498,12 @@ proc loadInitialOptions(self: WelcomeScreenComponent) = ), WelcomeScreenOption( name: "Open online trace", - inactive: true, - command: proc = discard + command: proc = + self.openOnlineTrace = true + self.welcomeScreen = false + self.newDownload = NewDownloadRecord( + args: @[] + ) ), WelcomeScreenOption( name: "CodeTracer shell", @@ -421,13 +551,15 @@ method render*(self: WelcomeScreenComponent): VNode = self.loadInitialOptions() buildHtml(tdiv()): - if self.welcomeScreen or self.newRecordScreen: + if self.welcomeScreen or self.newRecordScreen or self.openOnlineTrace: tdiv(class = "welcome-screen-wrapper"): windowMenu(data, true) if self.welcomeScreen: welcomeScreenView(self) elif self.newRecordScreen: newRecordView(self) + elif self.openOnlineTrace: + onlineTraceView(self) if self.loading: loadingOverlay(self) diff --git a/src/frontend/ui_js.nim b/src/frontend/ui_js.nim index 62c3b209..41e960c2 100644 --- a/src/frontend/ui_js.nim +++ b/src/frontend/ui_js.nim @@ -1090,6 +1090,9 @@ proc configureIPC(data: Data) = "follow-history" + "uploaded-trace-received" + "deleted-online-trace-received" + duration("configureIPCRun") proc zoomInEditors*(data: Data) = diff --git a/src/frontend/utils.nim b/src/frontend/utils.nim index 71682208..fd5c1848 100644 --- a/src/frontend/utils.nim +++ b/src/frontend/utils.nim @@ -1312,3 +1312,9 @@ proc clearViewZones*(self: EditorViewComponent) = self.monacoEditor.changeViewZones do (view: js): for viewZone in self.viewZones: view.removeZone(viewZone) + +proc resetView*(self: WelcomeScreenComponent) = + self.loading = false + self.welcomeScreen = false + self.newRecordScreen = false + self.openOnlineTrace = false