Compare commits
2 Commits
5aef38a99d
...
104c083995
| Author | SHA1 | Date | |
|---|---|---|---|
| 104c083995 | |||
| 189ca80343 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lms",
|
"name": "lms",
|
||||||
"version": "1.1.22",
|
"version": "1.1.23",
|
||||||
"description": "Autonomy Studio LMS Electron app",
|
"description": "Autonomy Studio LMS Electron app",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
+62
-9
@@ -954,6 +954,35 @@ function customModuleConfigContent(input: CustomModuleScaffoldInput): string {
|
|||||||
}, null, 2)}\n`
|
}, null, 2)}\n`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildCustomModuleWorkspaceFiles(input: CustomModuleScaffoldInput): Record<string, string> {
|
||||||
|
const module = validateCustomModuleScaffold(input)
|
||||||
|
const header = [
|
||||||
|
'#!/usr/bin/env bash',
|
||||||
|
'set -euo pipefail',
|
||||||
|
'',
|
||||||
|
'MODULE_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"',
|
||||||
|
'RUNTIME_ROOT="${AUTONOMY_STUDIO_RUNTIME_ROOT:-$HOME/autonomy-studio/runtime/nav2}"',
|
||||||
|
'FOLDER_NAME="$(basename "$MODULE_PATH")"',
|
||||||
|
`DEFAULT_IMAGE=${quoteShellSingle(module.dockerImage || 'future-nav2:humble')}`,
|
||||||
|
'IMAGE="${NAV2_DOCKER_IMAGE:-$DEFAULT_IMAGE}"',
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
'.autonomy-studio-module.json': customModuleConfigContent(module),
|
||||||
|
'.gitignore': 'build/\ninstall/\nlog/\n__pycache__/\n*.py[cod]\n',
|
||||||
|
'build.sh': [...header,
|
||||||
|
'exec bash "$RUNTIME_ROOT/docker/docker_build_nav2.sh" "$(dirname "$MODULE_PATH")" "$FOLDER_NAME" "$IMAGE" "${ROS_DOMAIN_ID:-0}" "$@"',
|
||||||
|
'',
|
||||||
|
].join('\n'),
|
||||||
|
'run.sh': [...header,
|
||||||
|
'CONTAINER_NAME="${FOLDER_NAME#\\[}"',
|
||||||
|
'CONTAINER_NAME="${CONTAINER_NAME/\\]/-}"',
|
||||||
|
'CONTAINER_NAME="${CONTAINER_NAME%_ws}"',
|
||||||
|
'exec bash "$RUNTIME_ROOT/docker/docker_run_nav2.sh" "$CONTAINER_NAME" "$(dirname "$MODULE_PATH")" "$FOLDER_NAME" "$IMAGE" "${ROS_DOMAIN_ID:-0}" "${GPU_ENABLED:-false}" "$@"',
|
||||||
|
'',
|
||||||
|
].join('\n'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveCustomModuleImportSource(sourcePath: string): Promise<CustomModuleImportSource> {
|
async function resolveCustomModuleImportSource(sourcePath: string): Promise<CustomModuleImportSource> {
|
||||||
const selectedPath = sourcePath.trim()
|
const selectedPath = sourcePath.trim()
|
||||||
if (!selectedPath) throw new Error('불러올 모듈 폴더를 선택하세요.')
|
if (!selectedPath) throw new Error('불러올 모듈 폴더를 선택하세요.')
|
||||||
@@ -1064,16 +1093,23 @@ async function importCustomWorkspaceModule(
|
|||||||
|
|
||||||
await mkdir(resolvedWorkspaceRoot, { recursive: true })
|
await mkdir(resolvedWorkspaceRoot, { recursive: true })
|
||||||
try {
|
try {
|
||||||
await cp(source.sourcePath, moduleRoot, {
|
const packageRoot = join(moduleRoot, 'src', module.packageName)
|
||||||
|
await mkdir(dirname(packageRoot), { recursive: true })
|
||||||
|
await cp(source.sourcePath, packageRoot, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
errorOnExist: true,
|
errorOnExist: true,
|
||||||
force: false,
|
force: false,
|
||||||
filter: (currentPath) => {
|
filter: (currentPath) => {
|
||||||
const pathParts = relative(source.sourcePath, currentPath).split(/[\\/]/).filter(Boolean)
|
const pathParts = relative(source.sourcePath, currentPath).split(/[\\/]/).filter(Boolean)
|
||||||
return !pathParts.some((part) => ignoredCustomModuleImportDirectories.has(part))
|
return !pathParts.some((part) => ignoredCustomModuleImportDirectories.has(part))
|
||||||
|
&& relative(source.sourcePath, currentPath) !== '.autonomy-studio-module.json'
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await writeFile(resolve(moduleRoot, '.autonomy-studio-module.json'), customModuleConfigContent(module), { encoding: 'utf8', flag: 'wx' })
|
for (const [fileName, content] of Object.entries(buildCustomModuleWorkspaceFiles(module))) {
|
||||||
|
await writeFile(join(moduleRoot, fileName), content, {
|
||||||
|
encoding: 'utf8', flag: 'wx', mode: fileName.endsWith('.sh') ? 0o755 : 0o644,
|
||||||
|
})
|
||||||
|
}
|
||||||
await runCommand('git', ['init', '--initial-branch=main'], moduleRoot, undefined, 15_000).catch(() => undefined)
|
await runCommand('git', ['init', '--initial-branch=main'], moduleRoot, undefined, 15_000).catch(() => undefined)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await rm(moduleRoot, { recursive: true, force: true }).catch(() => undefined)
|
await rm(moduleRoot, { recursive: true, force: true }).catch(() => undefined)
|
||||||
@@ -1086,9 +1122,7 @@ function buildCustomModuleFiles(input: CustomModuleScaffoldInput): Record<string
|
|||||||
const module = validateCustomModuleScaffold(input)
|
const module = validateCustomModuleScaffold(input)
|
||||||
const executable = `${module.packageName}_node`
|
const executable = `${module.packageName}_node`
|
||||||
const nodePath = `${module.packageName}/node.py`
|
const nodePath = `${module.packageName}/node.py`
|
||||||
return {
|
const packageFiles = {
|
||||||
'.autonomy-studio-module.json': customModuleConfigContent(module),
|
|
||||||
'.gitignore': 'build/\ninstall/\nlog/\n__pycache__/\n*.py[cod]\n',
|
|
||||||
'package.xml': [
|
'package.xml': [
|
||||||
'<?xml version="1.0"?>',
|
'<?xml version="1.0"?>',
|
||||||
'<package format="3">',
|
'<package format="3">',
|
||||||
@@ -1161,6 +1195,12 @@ function buildCustomModuleFiles(input: CustomModuleScaffoldInput): Record<string
|
|||||||
'',
|
'',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
...buildCustomModuleWorkspaceFiles(module),
|
||||||
|
...Object.fromEntries(Object.entries(packageFiles).map(([path, content]) => [
|
||||||
|
`src/${module.packageName}/${path}`, content,
|
||||||
|
])),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createCustomWorkspaceModule(
|
async function createCustomWorkspaceModule(
|
||||||
@@ -1172,12 +1212,16 @@ async function createCustomWorkspaceModule(
|
|||||||
if (!resolvedWorkspaceRoot) throw new Error('워크스페이스 경로가 설정되어 있지 않습니다.')
|
if (!resolvedWorkspaceRoot) throw new Error('워크스페이스 경로가 설정되어 있지 않습니다.')
|
||||||
const moduleRoot = resolve(resolvedWorkspaceRoot, module.folderName)
|
const moduleRoot = resolve(resolvedWorkspaceRoot, module.folderName)
|
||||||
const files = buildCustomModuleFiles(module)
|
const files = buildCustomModuleFiles(module)
|
||||||
|
const legacyLayout = existsSync(join(moduleRoot, 'package.xml'))
|
||||||
await mkdir(moduleRoot, { recursive: true })
|
await mkdir(moduleRoot, { recursive: true })
|
||||||
for (const [relativePath, content] of Object.entries(files)) {
|
for (const [relativePath, content] of Object.entries(files)) {
|
||||||
const targetPath = resolve(moduleRoot, relativePath)
|
const filePath = legacyLayout && relativePath.startsWith(`src/${module.packageName}/`)
|
||||||
|
? relativePath.slice(`src/${module.packageName}/`.length)
|
||||||
|
: relativePath
|
||||||
|
const targetPath = resolve(moduleRoot, filePath)
|
||||||
if (targetPath !== moduleRoot && !targetPath.startsWith(`${moduleRoot}/`)) throw new Error('허용되지 않은 모듈 파일 경로입니다.')
|
if (targetPath !== moduleRoot && !targetPath.startsWith(`${moduleRoot}/`)) throw new Error('허용되지 않은 모듈 파일 경로입니다.')
|
||||||
await mkdir(dirname(targetPath), { recursive: true })
|
await mkdir(dirname(targetPath), { recursive: true })
|
||||||
await writeFile(targetPath, content, { encoding: 'utf8', flag: 'wx' }).catch((error: NodeJS.ErrnoException) => {
|
await writeFile(targetPath, content, { encoding: 'utf8', flag: 'wx', mode: filePath.endsWith('.sh') ? 0o755 : 0o644 }).catch((error: NodeJS.ErrnoException) => {
|
||||||
if (error.code !== 'EEXIST') throw error
|
if (error.code !== 'EEXIST') throw error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1192,7 +1236,7 @@ async function createRemoteCustomWorkspaceModule(
|
|||||||
const target = normalizeRemoteTarget(targetInput)
|
const target = normalizeRemoteTarget(targetInput)
|
||||||
const module = validateCustomModuleScaffold(input)
|
const module = validateCustomModuleScaffold(input)
|
||||||
const files = buildCustomModuleFiles(module)
|
const files = buildCustomModuleFiles(module)
|
||||||
const payload = encodeBase64(JSON.stringify({ folderName: module.folderName, files }))
|
const payload = encodeBase64(JSON.stringify({ folderName: module.folderName, packageName: module.packageName, files }))
|
||||||
const output = await runSshOutput(target, [
|
const output = await runSshOutput(target, [
|
||||||
'set -e',
|
'set -e',
|
||||||
`ROOT=${quoteShellSingle(target.workspaceRoot)}`,
|
`ROOT=${quoteShellSingle(target.workspaceRoot)}`,
|
||||||
@@ -1206,12 +1250,16 @@ async function createRemoteCustomWorkspaceModule(
|
|||||||
'target = os.path.abspath(os.path.join(root, folder))',
|
'target = os.path.abspath(os.path.join(root, folder))',
|
||||||
'if target == root or not target.startswith(root + os.sep): raise SystemExit("invalid module path")',
|
'if target == root or not target.startswith(root + os.sep): raise SystemExit("invalid module path")',
|
||||||
'os.makedirs(target, exist_ok=True)',
|
'os.makedirs(target, exist_ok=True)',
|
||||||
|
'legacy = os.path.isfile(os.path.join(target, "package.xml"))',
|
||||||
|
'prefix = "src/" + payload["packageName"] + "/"',
|
||||||
'for relative, content in payload["files"].items():',
|
'for relative, content in payload["files"].items():',
|
||||||
|
' if legacy and relative.startswith(prefix): relative = relative[len(prefix):]',
|
||||||
' path = os.path.abspath(os.path.join(target, relative))',
|
' path = os.path.abspath(os.path.join(target, relative))',
|
||||||
' if not path.startswith(target + os.sep): raise SystemExit("invalid module file path")',
|
' if not path.startswith(target + os.sep): raise SystemExit("invalid module file path")',
|
||||||
' os.makedirs(os.path.dirname(path), exist_ok=True)',
|
' os.makedirs(os.path.dirname(path), exist_ok=True)',
|
||||||
' try:',
|
' try:',
|
||||||
' with open(path, "x", encoding="utf-8") as fp: fp.write(content)',
|
' with open(path, "x", encoding="utf-8") as fp: fp.write(content)',
|
||||||
|
' if relative in ("build.sh", "run.sh"): os.chmod(path, 0o755)',
|
||||||
' except FileExistsError:',
|
' except FileExistsError:',
|
||||||
' pass',
|
' pass',
|
||||||
'print(target)',
|
'print(target)',
|
||||||
@@ -4572,9 +4620,14 @@ function buildNav2DockerBuildScript(): string {
|
|||||||
'',
|
'',
|
||||||
'MODULE_PATH="$MODULE_ROOT/$MODULE_FOLDER"',
|
'MODULE_PATH="$MODULE_ROOT/$MODULE_FOLDER"',
|
||||||
'if [[ "$MODULE_FOLDER" == *lms-nav2-custom* ]]; then',
|
'if [[ "$MODULE_FOLDER" == *lms-nav2-custom* ]]; then',
|
||||||
' test -f "$MODULE_PATH/package.xml" || { echo "Custom module source is missing: $MODULE_FOLDER" >&2; exit 1; }',
|
' if [ -f "$MODULE_PATH/src/$TARGET/package.xml" ]; then',
|
||||||
|
' SOURCE_BASE_PATH=src',
|
||||||
|
' elif [ -f "$MODULE_PATH/package.xml" ]; then',
|
||||||
' SOURCE_BASE_PATH=.',
|
' SOURCE_BASE_PATH=.',
|
||||||
' else',
|
' else',
|
||||||
|
' echo "Custom module source is missing: $MODULE_FOLDER" >&2; exit 1',
|
||||||
|
' fi',
|
||||||
|
'else',
|
||||||
' test -d "$MODULE_PATH/src" || { echo "Module source is missing: $MODULE_FOLDER" >&2; exit 1; }',
|
' test -d "$MODULE_PATH/src" || { echo "Module source is missing: $MODULE_FOLDER" >&2; exit 1; }',
|
||||||
' SOURCE_BASE_PATH=src',
|
' SOURCE_BASE_PATH=src',
|
||||||
'fi',
|
'fi',
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ export function useWorkspaceModuleList({
|
|||||||
const moduleInputsRef = useRef({ curriculumModules, applyModuleParameterOverrides })
|
const moduleInputsRef = useRef({ curriculumModules, applyModuleParameterOverrides })
|
||||||
const loadSequenceRef = useRef(0)
|
const loadSequenceRef = useRef(0)
|
||||||
const moduleLoadInFlightRef = useRef(false)
|
const moduleLoadInFlightRef = useRef(false)
|
||||||
|
const queuedModuleLoadRef = useRef<(() => void) | null>(null)
|
||||||
|
const workspaceScopeKey = remoteWorkspace ? `remote:${remoteWorkspaceTargetKey}` : `local:${clonePathPreview}`
|
||||||
|
const previousWorkspaceScopeRef = useRef(workspaceScopeKey)
|
||||||
const pendingModuleStatesRef = useRef(new Map<string, { state: ModuleRow['state']; expiresAt: number }>())
|
const pendingModuleStatesRef = useRef(new Map<string, { state: ModuleRow['state']; expiresAt: number }>())
|
||||||
|
|
||||||
const moduleStateKey = (module: ModuleRow): string => module.folderName ?? module.name
|
const moduleStateKey = (module: ModuleRow): string => module.folderName ?? module.name
|
||||||
@@ -173,7 +176,10 @@ export function useWorkspaceModuleList({
|
|||||||
const loadAvailableModules = useCallback(async ({ silent = false }: LoadAvailableModulesOptions = {}): Promise<void> => {
|
const loadAvailableModules = useCallback(async ({ silent = false }: LoadAvailableModulesOptions = {}): Promise<void> => {
|
||||||
// Remote probes open an SSH session. Do not let polling overlap a slow
|
// Remote probes open an SSH session. Do not let polling overlap a slow
|
||||||
// probe, otherwise every response becomes stale and loading never settles.
|
// probe, otherwise every response becomes stale and loading never settles.
|
||||||
if (moduleLoadInFlightRef.current) return
|
if (moduleLoadInFlightRef.current) {
|
||||||
|
queuedModuleLoadRef.current = () => { void loadAvailableModules({ silent }) }
|
||||||
|
return
|
||||||
|
}
|
||||||
moduleLoadInFlightRef.current = true
|
moduleLoadInFlightRef.current = true
|
||||||
const requestSequence = ++loadSequenceRef.current
|
const requestSequence = ++loadSequenceRef.current
|
||||||
if (!silent) setModulesLoading(true)
|
if (!silent) setModulesLoading(true)
|
||||||
@@ -212,6 +218,9 @@ export function useWorkspaceModuleList({
|
|||||||
} finally {
|
} finally {
|
||||||
moduleLoadInFlightRef.current = false
|
moduleLoadInFlightRef.current = false
|
||||||
if (!silent && requestSequence === loadSequenceRef.current) setModulesLoading(false)
|
if (!silent && requestSequence === loadSequenceRef.current) setModulesLoading(false)
|
||||||
|
const queuedLoad = queuedModuleLoadRef.current
|
||||||
|
queuedModuleLoadRef.current = null
|
||||||
|
queuedLoad?.()
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
applyPendingModuleStates,
|
applyPendingModuleStates,
|
||||||
@@ -223,24 +232,38 @@ export function useWorkspaceModuleList({
|
|||||||
])
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// A response for an earlier module set must not replace newly added rows.
|
||||||
|
++loadSequenceRef.current
|
||||||
|
const sameWorkspace = previousWorkspaceScopeRef.current === workspaceScopeKey
|
||||||
|
previousWorkspaceScopeRef.current = workspaceScopeKey
|
||||||
|
if (!sameWorkspace) pendingModuleStatesRef.current.clear()
|
||||||
const cachedModules = readRemoteModuleCache(moduleCacheKey)
|
const cachedModules = readRemoteModuleCache(moduleCacheKey)
|
||||||
if (cachedModules) {
|
setAvailableModules((current) => {
|
||||||
const modules = applyPendingModuleStates(
|
const previous = new Map((sameWorkspace && current.length ? current : cachedModules ?? [])
|
||||||
moduleInputsRef.current.applyModuleParameterOverrides(cachedModules).sort(compareModulePriority),
|
.map((module) => [module.folderName ?? module.name, module]))
|
||||||
)
|
const modules = makeUninstalledModuleRows(moduleInputsRef.current.curriculumModules).map((module) => {
|
||||||
setAvailableModules(modules)
|
const existing = previous.get(module.folderName ?? module.name)
|
||||||
|
return existing ? {
|
||||||
|
...module,
|
||||||
|
state: existing.state,
|
||||||
|
workspacePath: existing.workspacePath,
|
||||||
|
parameters: existing.parameters,
|
||||||
|
cpu: existing.cpu,
|
||||||
|
memory: existing.memory,
|
||||||
|
runtime: existing.runtime,
|
||||||
|
} : module
|
||||||
|
})
|
||||||
|
return applyPendingModuleStates(moduleInputsRef.current.applyModuleParameterOverrides(modules))
|
||||||
|
})
|
||||||
setModuleListError(null)
|
setModuleListError(null)
|
||||||
setModulesLoading(false)
|
setModulesLoading(false)
|
||||||
if (skipInitialModuleLoad) return
|
if (!(cachedModules && skipInitialModuleLoad)) void loadAvailableModules()
|
||||||
}
|
|
||||||
|
|
||||||
if (!cachedModules) {
|
return () => {
|
||||||
setAvailableModules(makeUninstalledModuleRows(moduleInputsRef.current.curriculumModules))
|
++loadSequenceRef.current
|
||||||
setModuleListError(null)
|
queuedModuleLoadRef.current = null
|
||||||
}
|
}
|
||||||
|
}, [applyPendingModuleStates, loadAvailableModules, moduleCacheKey, skipInitialModuleLoad, workspaceScopeKey])
|
||||||
void loadAvailableModules()
|
|
||||||
}, [applyPendingModuleStates, loadAvailableModules, moduleCacheKey, remoteWorkspaceTargetKey, skipInitialModuleLoad])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pollingActive) return
|
if (!pollingActive) return
|
||||||
|
|||||||
Reference in New Issue
Block a user