shellbase 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -6
- package/dist/session.js +19 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,10 +107,52 @@ systemctl --user enable --now shellbase.service
|
|
|
107
107
|
작업 스케줄러(Task Scheduler)에서 "로그온 시 시작" + "실패 시 다시 시작" 옵션으로 등록하면 비슷하게
|
|
108
108
|
됩니다. 아직 저희가 Windows에서 직접 검증은 못 했어요.
|
|
109
109
|
|
|
110
|
-
### Docker
|
|
110
|
+
### Docker (Linux, 실제 검증됨)
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
컨테이너 안은 이 컴퓨터의 실제 환경과 분리돼 있어서, 홈 폴더를 통째로 볼륨(`-v`)으로 연결해야
|
|
113
|
+
git 설정·SSH 키·진짜 작업 파일들이 컨테이너 밖과 똑같이 보여요. 아래는 실제로 빌드해서 재부팅 시나리오
|
|
114
|
+
까지 검증한 구성이에요.
|
|
115
|
+
|
|
116
|
+
```dockerfile
|
|
117
|
+
# Dockerfile
|
|
118
|
+
FROM node:22-bookworm-slim
|
|
119
|
+
|
|
120
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
121
|
+
build-essential python3 git openssh-client ca-certificates \
|
|
122
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
123
|
+
|
|
124
|
+
RUN npm install -g shellbase@latest
|
|
125
|
+
|
|
126
|
+
ENTRYPOINT ["shellbase"]
|
|
127
|
+
CMD ["start"]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
docker build -t shellbase-agent .
|
|
132
|
+
|
|
133
|
+
docker run -d \
|
|
134
|
+
--name shellbase-agent \
|
|
135
|
+
--restart unless-stopped \
|
|
136
|
+
--user $(id -u):$(id -g) \
|
|
137
|
+
-v $HOME:$HOME \
|
|
138
|
+
-e HOME=$HOME \
|
|
139
|
+
shellbase-agent \
|
|
140
|
+
start --dir $HOME --name "내 서버 (Docker)"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
> **주의할 점 3가지 (직접 겪은 문제라 적어둡니다):**
|
|
144
|
+
> 1. **Node 22 이상 이미지를 써야 해요.** `node:20-...` 로 빌드하면 실시간 접속이 조용히 SSE
|
|
145
|
+
> 모드로 떨어져서 `shellbase start` 가 "네트워크가 막혔다"는 헷갈리는 에러를 내고 죽어요 —
|
|
146
|
+
> (0.1.5부터는 시작 시 Node 버전을 먼저 확인해서 바로 알려줘요.)
|
|
147
|
+
> 2. **`--user $(id -u):$(id -g)` 를 꼭 넣으세요.** 안 넣으면 컨테이너가 root로 돌아서, 터미널
|
|
148
|
+
> 안에서 만들거나 수정한 파일이 호스트에서는 root 소유가 돼버려요(내 계정으로 못 지우고 수정
|
|
149
|
+
> 못 하는 파일이 생김).
|
|
150
|
+
> 3. **detached 컨테이너는 접속 승인을 물어볼 방법이 없어요** — 터미널이 배경에 떠 있어서 `y` 를
|
|
151
|
+
> 입력할 TTY가 없거든요. `--auto-approve` 를 커맨드에 박아넣거나(위 예시엔 안 넣었어요), 폰 목록
|
|
152
|
+
> 화면의 "새 세션 자동 승인" 토글을 컨테이너 **시작 전에** 미리 켜두세요(계정 단위 설정이라 이
|
|
153
|
+
> 컴퓨터에도 그대로 적용돼요 — 시작한 뒤에 켜면 다음 재시작부터 적용됩니다).
|
|
154
|
+
|
|
155
|
+
재부팅 시 자동 복구는 도커 데몬이 담당해요 — `systemctl is-enabled docker` 로 `enabled` 인지
|
|
156
|
+
확인하세요(보통 기본값). `unless-stopped` 는 컴퓨터가 재부팅되면서 데몬이 다시 뜰 때 "그때 돌아가고
|
|
157
|
+
있던" 컨테이너를 자동으로 복구해줘요(직접 `docker stop` 으로 멈춰뒀던 건 복구 안 해요 — 의도된
|
|
158
|
+
동작이에요). 강제 종료(`kill -9`, OOM 등)로 죽는 경우도 자동 재시작되는 것까지 확인했어요.
|
package/dist/session.js
CHANGED
|
@@ -64,13 +64,22 @@ export async function startSession(options) {
|
|
|
64
64
|
cwd,
|
|
65
65
|
env: process.env,
|
|
66
66
|
});
|
|
67
|
-
// PTY 출력은
|
|
67
|
+
// PTY 출력은 모아서 전송 — 서버가 전송 속도를 최소 100ms 간격으로 제한해서(RATE_LIMITED), 그보다
|
|
68
|
+
// 짧은 간격으로 보내면 출력이 많은 순간(로그 스크롤 등)에 전송이 거부될 수 있음. 그렇다고 매번
|
|
69
|
+
// 고정 간격을 기다리면 한두 글자짜리 출력도 불필요하게 지연되니, "마지막 전송 후 이 간격이
|
|
70
|
+
// 지났으면 바로 보내고, 아니면 그만큼만 기다렸다 모아서 보낸다" 방식으로 평소 지연은 최소화하고
|
|
71
|
+
// 출력이 몰릴 때만 자동으로 묶이게 한다.
|
|
72
|
+
const MIN_SEND_INTERVAL_MS = 120;
|
|
68
73
|
let outBuffer = '';
|
|
74
|
+
let lastSentAt = 0;
|
|
75
|
+
let pendingFlush = null;
|
|
69
76
|
const flush = async () => {
|
|
77
|
+
pendingFlush = null;
|
|
70
78
|
if (!outBuffer)
|
|
71
79
|
return;
|
|
72
80
|
const chunk = outBuffer;
|
|
73
81
|
outBuffer = '';
|
|
82
|
+
lastSentAt = Date.now();
|
|
74
83
|
try {
|
|
75
84
|
await channel.send({
|
|
76
85
|
kind: 'output',
|
|
@@ -82,12 +91,18 @@ export async function startSession(options) {
|
|
|
82
91
|
console.error('출력 전송 실패:', err.message);
|
|
83
92
|
}
|
|
84
93
|
};
|
|
85
|
-
const
|
|
94
|
+
const scheduleFlush = () => {
|
|
95
|
+
if (pendingFlush !== null)
|
|
96
|
+
return;
|
|
97
|
+
const wait = Math.max(0, MIN_SEND_INTERVAL_MS - (Date.now() - lastSentAt));
|
|
98
|
+
pendingFlush = setTimeout(() => void flush(), wait);
|
|
99
|
+
};
|
|
86
100
|
// 최근 출력을 조금 들고 있다가, 모바일이 (재)접속했을 때 놓친 화면 대신 다시 보여줌
|
|
87
101
|
let scrollback = '';
|
|
88
102
|
ptyProcess.onData((data) => {
|
|
89
103
|
outBuffer += data;
|
|
90
104
|
scrollback = (scrollback + data).slice(-SCROLLBACK_MAX);
|
|
105
|
+
scheduleFlush();
|
|
91
106
|
});
|
|
92
107
|
// 모바일이 접속을 시도하면 데스크톱에서 승인해야만 입력이 먹힘 (읽기/출력은 항상 흐름 —
|
|
93
108
|
// 이미 같은 계정으로 로그인했다는 전제이므로, 승인은 "지금 이 순간 내가 타이핑을 넘겨줄지"
|
|
@@ -141,7 +156,8 @@ export async function startSession(options) {
|
|
|
141
156
|
if (shuttingDown)
|
|
142
157
|
return;
|
|
143
158
|
shuttingDown = true;
|
|
144
|
-
|
|
159
|
+
if (pendingFlush !== null)
|
|
160
|
+
clearTimeout(pendingFlush);
|
|
145
161
|
clearInterval(heartbeat);
|
|
146
162
|
await flush();
|
|
147
163
|
stopMessages();
|