feat: add weekly curriculum cover image upload and display
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
const image = 'future-nav2-camera-test:humble-jetson_orin'
|
||||
|
||||
export function cameraRuntimeArtifacts<T extends { image?: string | null }>(
|
||||
folder: string,
|
||||
configured: T[] | null | undefined,
|
||||
): T[] | null | undefined {
|
||||
if (!configured) return configured
|
||||
const camera = folder.includes('lms-nav2-camera-test')
|
||||
// The optional camera image must not be downloaded for other Nav2 modules.
|
||||
return configured.filter((item) => (item.image?.trim() === image) === camera)
|
||||
}
|
||||
+43
-41
@@ -1,5 +1,7 @@
|
||||
import { app, BrowserWindow, dialog, Menu, globalShortcut, ipcMain, screen, shell } from 'electron'
|
||||
import { checkDockerStatus } from './dockerStatus'
|
||||
import { cameraRuntimeArtifacts } from './cameraRuntime'
|
||||
import { remoteModuleFilesScript } from './remoteModuleFiles'
|
||||
import { resolveContainerTerminal } from './containerTerminal'
|
||||
import { yahboomDevicePreflight, yahboomModuleCases, yahboomRuntimeImage, yahboomRuntimeArtifacts, yahboomSimulation } from './yahboomRuntime'
|
||||
import { visualizationProgress, reportVisualizationProgress } from './visualizationProgress'
|
||||
@@ -142,6 +144,7 @@ function getBundledZstdPath(): string | null {
|
||||
}
|
||||
|
||||
type CachedModuleFiles = {
|
||||
filePaths?: string[]
|
||||
root: string | null
|
||||
absoluteRoot: string | null
|
||||
files: Record<string, string>
|
||||
@@ -4598,6 +4601,7 @@ function buildNav2DockerRunScript(): string {
|
||||
'else',
|
||||
'case "$MODULE_FOLDER" in',
|
||||
...yahboomModuleCases(),
|
||||
' *lms-nav2-camera-test*) PACKAGE=camera_test_pkg; LAUNCH=camera.launch.py ;;',
|
||||
' *lms-nav2-mkmini*) PACKAGE=vehicle_pkg; LAUNCH=vehicle.launch.py ;;',
|
||||
' *lms-nav2-gazebo-simulator*) PACKAGE=nav2_turtlebot_simulator; LAUNCH=simulator.launch.py ;;',
|
||||
' *lms-nav2-gazebo-vehicle*) PACKAGE=vehicle_pkg; LAUNCH=vehicle.launch.py ;;',
|
||||
@@ -4635,6 +4639,12 @@ function buildNav2DockerRunScript(): string {
|
||||
'',
|
||||
'test -f "$MODULE_ROOT/$MODULE_FOLDER/install/setup.bash" || { echo "Module is not built: $MODULE_FOLDER" >&2; exit 1; }',
|
||||
...yahboomDevicePreflight(),
|
||||
'CAMERA_DEVICE=""',
|
||||
'if [[ "$MODULE_FOLDER" == *lms-nav2-camera-test* ]]; then',
|
||||
' CAMERA_DEVICE=/dev/video0',
|
||||
' for arg in "$@"; do case "$arg" in video_device:=*) CAMERA_DEVICE=${arg#video_device:=} ;; esac; done',
|
||||
' [[ "$CAMERA_DEVICE" =~ ^/dev/video[0-9]+$ ]] && [ -c "$CAMERA_DEVICE" ] || { echo "Camera device not found: $CAMERA_DEVICE" >&2; exit 1; }',
|
||||
'fi',
|
||||
'docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true',
|
||||
'',
|
||||
'args=(',
|
||||
@@ -4652,6 +4662,12 @@ function buildNav2DockerRunScript(): string {
|
||||
' -v /etc/localtime:/etc/localtime:ro',
|
||||
')',
|
||||
'if [ -n "$YAHBOOM_DEVICE" ]; then args+=(--device "$YAHBOOM_DEVICE:$YAHBOOM_DEVICE"); fi',
|
||||
'if [ -n "$CAMERA_DEVICE" ]; then',
|
||||
' args+=(--device "$CAMERA_DEVICE:$CAMERA_DEVICE")',
|
||||
' for capture_device in /dev/capture-vi-channel*; do',
|
||||
' if [ -c "$capture_device" ]; then args+=(--device "$capture_device:$capture_device"); fi',
|
||||
' done',
|
||||
'fi',
|
||||
'if [[ "$GPU_ENABLED" == "true" || "$GPU_ENABLED" == "1" ]]; then',
|
||||
' args+=(--gpus all -e NVIDIA_VISIBLE_DEVICES=all -e NVIDIA_DRIVER_CAPABILITIES=compute,utility,graphics)',
|
||||
'fi',
|
||||
@@ -4695,6 +4711,7 @@ function buildNav2DockerBuildScript(): string {
|
||||
'else',
|
||||
'case "$MODULE_FOLDER" in',
|
||||
...yahboomModuleCases(true),
|
||||
' *lms-nav2-camera-test*) TARGET=camera_test_pkg ;;',
|
||||
' *lms-nav2-mkmini*) TARGET=vehicle_pkg ;;',
|
||||
' *lms-nav2-gazebo-simulator*) TARGET=nav2_turtlebot_simulator ;;',
|
||||
' *lms-nav2-gazebo-vehicle*) TARGET=vehicle_pkg ;;',
|
||||
@@ -5051,6 +5068,7 @@ async function resolveLocalModuleRuntime(
|
||||
rosParams: string,
|
||||
runtimeImages?: RuntimeImageArtifact[] | null,
|
||||
): Promise<{ image: string; artifacts: RuntimeImageArtifact[] | null | undefined }> {
|
||||
runtimeImages = cameraRuntimeArtifacts(folderName, runtimeImages)
|
||||
if (yahboomSimulation(folderName, rosParams)) {
|
||||
const artifact = runtimeImages?.find((item) =>
|
||||
item.image?.trim() === defaultNav2GazeboRuntimeImage.image && item.url?.trim())
|
||||
@@ -5062,7 +5080,9 @@ async function resolveLocalModuleRuntime(
|
||||
)
|
||||
return {
|
||||
image,
|
||||
artifacts: yahboomRuntimeImage(folderName)
|
||||
artifacts: folderName.includes('lms-nav2-camera-test')
|
||||
? (runtimeImages ?? []).filter((item) => item.image?.trim() === image)
|
||||
: yahboomRuntimeImage(folderName)
|
||||
? yahboomRuntimeArtifacts(folderName, image, runtimeImages)
|
||||
: requiredRuntimeImages(runtimeImages, image),
|
||||
}
|
||||
@@ -5309,6 +5329,7 @@ async function startRemoteWorkspaceModule(
|
||||
if (existsOutput.trim() !== 'yes') throw new Error('설치된 모듈을 찾을 수 없습니다.')
|
||||
|
||||
const nav2Module = isNav2ModuleFolder(targetFolderName)
|
||||
runtimeImages = cameraRuntimeArtifacts(targetFolderName, runtimeImages)
|
||||
const runtimeImage = nav2Module
|
||||
? await resolveRemoteCustomModuleRuntimeImage(
|
||||
target,
|
||||
@@ -5320,7 +5341,9 @@ async function startRemoteWorkspaceModule(
|
||||
? await ensureRemoteNav2RuntimeAssets(target)
|
||||
: await ensureRemoteStudioRuntimeAssets(target)
|
||||
const fastDdsConfigPath = await prepareRemoteRosNetworkConfig(target, runtimeRoot, rosNetwork, rosDomainId)
|
||||
await prepareRemoteRuntimeImages(target, yahboomRuntimeArtifacts(targetFolderName, runtimeImage, runtimeImages))
|
||||
await prepareRemoteRuntimeImages(target, targetFolderName.includes('lms-nav2-camera-test')
|
||||
? (runtimeImages ?? []).filter((item) => item.image?.trim() === runtimeImage)
|
||||
: yahboomRuntimeArtifacts(targetFolderName, runtimeImage, runtimeImages))
|
||||
reportModuleProgress("모듈 실행 중...")
|
||||
const currentStatus = await runSshOutput(target, `timeout 12s docker ps -a --filter ${quoteShellSingle(`name=^/${dockerInfo.containerKey}$`)} --format "{{.Status}}" 2>/dev/null || true`, 20000)
|
||||
if (/^Up\b/i.test(currentStatus.trim())) return { ok: true, message: '이미 실행 중입니다.' }
|
||||
@@ -5403,6 +5426,7 @@ async function buildRemoteWorkspaceModule(
|
||||
): Promise<{ ok: boolean; message: string }> {
|
||||
const target = normalizeRemoteTarget(targetInput)
|
||||
const targetFolderName = folderName?.trim() || moduleName
|
||||
runtimeImages = cameraRuntimeArtifacts(targetFolderName, runtimeImages)
|
||||
const dockerInfo = parseModuleDockerInfo(targetFolderName)
|
||||
if (!dockerInfo || !isAllowedContainerName(dockerInfo.containerKey)) throw new Error('모듈 폴더명 형식이 올바르지 않습니다.')
|
||||
|
||||
@@ -5413,7 +5437,9 @@ async function buildRemoteWorkspaceModule(
|
||||
targetFolderName,
|
||||
yahboomRuntimeImage(targetFolderName) ?? await resolveRemoteNav2RuntimeImage(runtimeImages),
|
||||
)
|
||||
await prepareRemoteRuntimeImages(target, yahboomRuntimeArtifacts(targetFolderName, runtimeImage, runtimeImages))
|
||||
await prepareRemoteRuntimeImages(target, targetFolderName.includes('lms-nav2-camera-test')
|
||||
? (runtimeImages ?? []).filter((item) => item.image?.trim() === runtimeImage)
|
||||
: yahboomRuntimeArtifacts(targetFolderName, runtimeImage, runtimeImages))
|
||||
reportModuleProgress("모듈 빌드 중...")
|
||||
await runSsh(target, [
|
||||
'set -e',
|
||||
@@ -5723,44 +5749,11 @@ async function collectModuleFiles(moduleName: string, workspaceRoot?: string | n
|
||||
|
||||
async function collectRemoteModuleFiles(targetInput: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null): Promise<CachedModuleFiles> {
|
||||
const { target, moduleRoot } = await resolveRemoteGitModuleRoot(targetInput, moduleName, folderName)
|
||||
const skippedNames = Array.from(skippedCacheDirs).map(quoteShellSingle).join(' ')
|
||||
const script = [
|
||||
'set +e',
|
||||
`ROOT=${quoteShellSingle(moduleRoot)}`,
|
||||
`SKIPPED="${skippedNames}"`,
|
||||
'find "$ROOT" -mindepth 1 -maxdepth 10 \\( -name .git -o -name build -o -name install -o -name log -o -name node_modules -o -name docker \\) -prune -o -type d -printf "D\\t%P\\n" -o -type f -printf "F\\t%P\\t%s\\n" | while IFS="$(printf \'\\t\')" read -r KIND REL SIZE; do',
|
||||
' [ -z "$REL" ] && continue',
|
||||
' NAME="${REL##*/}"',
|
||||
' case "$KIND" in',
|
||||
' D) printf "D\\t%s\\n" "$(printf %s "$REL" | base64 -w0)" ;;',
|
||||
' F)',
|
||||
' CONTENT="QmluYXJ5IG9yIGxhcmdlIGZpbGUgcHJldmlldyBpcyBub3QgYXZhaWxhYmxlLg=="',
|
||||
' if [ "${SIZE:-0}" -le 1048576 ]; then',
|
||||
' case "$NAME" in',
|
||||
' *.c|*.cc|*.cpp|*.cxx|*.h|*.hpp|*.hh|*.py|*.xml|*.urdf|*.xacro|*.yaml|*.yml|*.json|*.txt|*.md|*.cmake|*.launch|*.sh|*.bash|*.toml|*.ini|*.cfg|*.rviz) CONTENT="$(base64 -w0 "$ROOT/$REL" 2>/dev/null)" ;;',
|
||||
' esac',
|
||||
' fi',
|
||||
' printf "F\\t%s\\t%s\\n" "$(printf %s "$REL" | base64 -w0)" "$CONTENT"',
|
||||
' ;;',
|
||||
' esac',
|
||||
'done'
|
||||
].join('\n')
|
||||
// Source transfers must not hold up module start/stop on the control queue.
|
||||
const output = await runUnqueuedSshOutput(target, script, 30000)
|
||||
const files: Record<string, string> = {}
|
||||
const folders: string[] = []
|
||||
|
||||
for (const line of output.split('\n')) {
|
||||
if (!line.trim()) continue
|
||||
const [kind, encodedPath = '', encodedContent = ''] = line.split('\t')
|
||||
const relativePath = decodeRemoteField(encodedPath)
|
||||
if (!relativePath) continue
|
||||
const rendererPath = `modules/${moduleName}/${relativePath}`
|
||||
if (kind === 'D') folders.push(rendererPath)
|
||||
if (kind === 'F') files[rendererPath] = decodeRemoteField(encodedContent)
|
||||
}
|
||||
|
||||
return { root: moduleRoot.split('/').pop() ?? moduleName, absoluteRoot: moduleRoot, files, folders }
|
||||
const output = await runUnqueuedSshOutput(target, remoteModuleFilesScript(moduleRoot, { action: 'list' }), 30000)
|
||||
const listing = JSON.parse(output) as { filePaths: string[]; folders: string[] }
|
||||
const prefix = (path: string): string => `modules/${moduleName}/${path}`
|
||||
return { root: moduleRoot.split('/').pop() ?? moduleName, absoluteRoot: moduleRoot,
|
||||
files: {}, filePaths: listing.filePaths.map(prefix), folders: listing.folders.map(prefix) }
|
||||
}
|
||||
|
||||
async function collectRootFiles(rootPath: string, rendererRoot: string): Promise<CachedModuleFiles> {
|
||||
@@ -6447,6 +6440,15 @@ function registerLocalWorkspaceHandlers(): void {
|
||||
ipcMain.handle('remote-workspace:git:submissions', (_event, target: RemoteWorkspaceTarget, modules: Array<{ moduleName: string; folderName?: string | null }>) => readRemoteWorkspaceGitSubmissions(target, modules))
|
||||
ipcMain.handle('remote-workspace:git:commit-diff', (_event, target: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null, commitHash?: string | null) => readRemoteWorkspaceGitCommitDiff(target, moduleName, folderName, commitHash))
|
||||
ipcMain.handle('remote-workspace:module-files:read', (_event, target: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null) => collectRemoteModuleFiles(target, moduleName, folderName))
|
||||
ipcMain.handle('remote-workspace:module-files:content', async (_event, target: RemoteWorkspaceTarget, moduleName: string, path: string, folderName?: string | null) => {
|
||||
const resolved = await resolveRemoteRendererModulePath(target, moduleName, path, folderName)
|
||||
return JSON.parse(await runUnqueuedSshOutput(resolved.target, remoteModuleFilesScript(resolved.moduleRoot, { action: 'content', path: resolved.relativePath }), 30000))
|
||||
})
|
||||
ipcMain.handle('remote-workspace:module-files:search', async (_event, target: RemoteWorkspaceTarget, moduleName: string, query: string, folderName?: string | null) => {
|
||||
const resolved = await resolveRemoteGitModuleRoot(target, moduleName, folderName)
|
||||
const results = JSON.parse(await runUnqueuedSshOutput(resolved.target, remoteModuleFilesScript(resolved.moduleRoot, { action: 'search', query }), 30000)) as Array<{ file: string; lineNumber: number; lineText: string }>
|
||||
return results.map((result) => ({ ...result, file: `modules/${moduleName}/${result.file}` }))
|
||||
})
|
||||
ipcMain.handle('remote-workspace:module-files:image', (_event, target: RemoteWorkspaceTarget, moduleName: string, rendererPath: string, folderName?: string | null) => readRemoteWorkspaceModuleImage(target, moduleName, rendererPath, folderName))
|
||||
ipcMain.handle('remote-workspace:module-files:create', async (_event, target: RemoteWorkspaceTarget, moduleName: string, rendererPath: string, kind: 'file' | 'folder', folderName?: string | null) => {
|
||||
const resolved = await resolveRemoteRendererModulePath(target, moduleName, rendererPath, folderName)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
type Request = { action: 'list' } | { action: 'content'; path: string } | { action: 'search'; query: string }
|
||||
|
||||
export function remoteModuleFilesScript(root: string, request: Request): string {
|
||||
const input = Buffer.from(JSON.stringify({ root, ...request })).toString('base64')
|
||||
return `python3 - '${input}' <<'PY'
|
||||
import base64, json, os, stat, sys
|
||||
args = json.loads(base64.b64decode(sys.argv[1]))
|
||||
root = os.path.realpath(args['root'])
|
||||
skip = {'.git', 'build', 'install', 'log', 'node_modules', 'docker'}
|
||||
extensions = {'.c', '.cc', '.cpp', '.cxx', '.h', '.hpp', '.hh', '.py', '.xml', '.urdf', '.xacro', '.yaml', '.yml', '.json', '.txt', '.md', '.cmake', '.launch', '.sh', '.bash', '.toml', '.ini', '.cfg', '.rviz'}
|
||||
placeholder = 'Binary or large file preview is not available.'
|
||||
def content(relative):
|
||||
if os.path.isabs(relative) or '..' in relative.split('/'):
|
||||
raise ValueError('Invalid module path')
|
||||
path = os.path.realpath(os.path.join(root, relative))
|
||||
if os.path.commonpath([root, path]) != root:
|
||||
raise ValueError('Invalid module path')
|
||||
info = os.stat(path)
|
||||
if not stat.S_ISREG(info.st_mode):
|
||||
raise ValueError('Not a regular file')
|
||||
if os.path.splitext(path)[1].lower() not in extensions or info.st_size > 1048576:
|
||||
return placeholder
|
||||
with open(path, 'rb') as fp:
|
||||
data = fp.read(1048577)
|
||||
if len(data) > 1048576 or b'\\x00' in data:
|
||||
return placeholder
|
||||
return data.decode('utf-8', errors='replace')
|
||||
def entries():
|
||||
def fail(error):
|
||||
raise error
|
||||
for current, dirs, files in os.walk(root, onerror=fail, followlinks=False):
|
||||
depth = len(os.path.relpath(current, root).split('/')) if current != root else 0
|
||||
dirs[:] = sorted(d for d in dirs if d not in skip and not os.path.islink(os.path.join(current, d)))
|
||||
if depth >= 10:
|
||||
dirs[:] = []
|
||||
continue
|
||||
for name in dirs:
|
||||
yield 'D', os.path.relpath(os.path.join(current, name), root)
|
||||
for name in sorted(files):
|
||||
path = os.path.join(current, name)
|
||||
if not os.path.islink(path) and os.path.isfile(path):
|
||||
yield 'F', os.path.relpath(path, root)
|
||||
if args['action'] == 'content':
|
||||
print(json.dumps({'content': content(args['path'])}))
|
||||
elif args['action'] == 'list':
|
||||
paths, folders = [], []
|
||||
for kind, path in entries():
|
||||
(folders if kind == 'D' else paths).append(path)
|
||||
print(json.dumps({'filePaths': paths, 'folders': folders}))
|
||||
else:
|
||||
query = args['query'].strip().lower()
|
||||
results = []
|
||||
if len(query) >= 2:
|
||||
for kind, path in entries():
|
||||
if kind != 'F':
|
||||
continue
|
||||
text = content(path)
|
||||
if text == placeholder:
|
||||
continue
|
||||
for number, line in enumerate(text.split('\\n'), 1):
|
||||
if query in line.lower():
|
||||
results.append({'file': path, 'lineNumber': number, 'lineText': (line.strip() or line)[:2000]})
|
||||
if len(results) >= 200:
|
||||
break
|
||||
if len(results) >= 200:
|
||||
break
|
||||
print(json.dumps(results))
|
||||
PY`
|
||||
}
|
||||
Reference in New Issue
Block a user