You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

BaseCommand.java 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. // Copyright (C) 2009 The Android Open Source Project
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package com.gitblit.transport.ssh.commands;
  15. import java.io.BufferedWriter;
  16. import java.io.IOException;
  17. import java.io.InputStream;
  18. import java.io.InterruptedIOException;
  19. import java.io.OutputStream;
  20. import java.io.OutputStreamWriter;
  21. import java.io.PrintWriter;
  22. import java.io.StringWriter;
  23. import java.util.concurrent.Future;
  24. import java.util.concurrent.atomic.AtomicReference;
  25. import org.apache.sshd.common.SshException;
  26. import org.apache.sshd.server.Command;
  27. import org.apache.sshd.server.Environment;
  28. import org.apache.sshd.server.ExitCallback;
  29. import org.apache.sshd.server.SessionAware;
  30. import org.apache.sshd.server.session.ServerSession;
  31. import org.kohsuke.args4j.Argument;
  32. import org.kohsuke.args4j.CmdLineException;
  33. import org.kohsuke.args4j.Option;
  34. import org.slf4j.Logger;
  35. import org.slf4j.LoggerFactory;
  36. import com.gitblit.Keys;
  37. import com.gitblit.utils.IdGenerator;
  38. import com.gitblit.utils.StringUtils;
  39. import com.gitblit.utils.WorkQueue;
  40. import com.gitblit.utils.WorkQueue.CancelableRunnable;
  41. import com.gitblit.utils.cli.CmdLineParser;
  42. import com.google.common.base.Charsets;
  43. import com.google.common.util.concurrent.Atomics;
  44. public abstract class BaseCommand implements Command, SessionAware {
  45. private static final Logger log = LoggerFactory.getLogger(BaseCommand.class);
  46. private static final int PRIVATE_STATUS = 1 << 30;
  47. public final static int STATUS_CANCEL = PRIVATE_STATUS | 1;
  48. public final static int STATUS_NOT_FOUND = PRIVATE_STATUS | 2;
  49. public final static int STATUS_NOT_ADMIN = PRIVATE_STATUS | 3;
  50. protected InputStream in;
  51. protected OutputStream out;
  52. protected OutputStream err;
  53. protected ExitCallback exit;
  54. protected ServerSession session;
  55. /** Ssh command context */
  56. private SshCommandContext ctx;
  57. /** Text of the command line which lead up to invoking this instance. */
  58. private String commandName = "";
  59. /** Unparsed command line options. */
  60. private String[] argv;
  61. /** The task, as scheduled on a worker thread. */
  62. private final AtomicReference<Future<?>> task;
  63. private final WorkQueue.Executor executor;
  64. public BaseCommand() {
  65. task = Atomics.newReference();
  66. IdGenerator gen = new IdGenerator();
  67. WorkQueue w = new WorkQueue(gen);
  68. this.executor = w.getDefaultQueue();
  69. }
  70. @Override
  71. public void setSession(final ServerSession session) {
  72. this.session = session;
  73. }
  74. @Override
  75. public void destroy() {
  76. log.debug("destroying " + getClass().getName());
  77. session = null;
  78. ctx = null;
  79. }
  80. protected static PrintWriter toPrintWriter(final OutputStream o) {
  81. return new PrintWriter(new BufferedWriter(new OutputStreamWriter(o, Charsets.UTF_8)));
  82. }
  83. @Override
  84. public abstract void start(Environment env) throws IOException;
  85. protected void provideStateTo(final BaseCommand cmd) {
  86. cmd.setContext(ctx);
  87. cmd.setInputStream(in);
  88. cmd.setOutputStream(out);
  89. cmd.setErrorStream(err);
  90. cmd.setExitCallback(exit);
  91. }
  92. public void setContext(SshCommandContext ctx) {
  93. this.ctx = ctx;
  94. }
  95. public SshCommandContext getContext() {
  96. return ctx;
  97. }
  98. @Override
  99. public void setInputStream(final InputStream in) {
  100. this.in = in;
  101. }
  102. @Override
  103. public void setOutputStream(final OutputStream out) {
  104. this.out = out;
  105. }
  106. @Override
  107. public void setErrorStream(final OutputStream err) {
  108. this.err = err;
  109. }
  110. @Override
  111. public void setExitCallback(final ExitCallback callback) {
  112. this.exit = callback;
  113. }
  114. protected String getName() {
  115. return commandName;
  116. }
  117. void setName(final String prefix) {
  118. this.commandName = prefix;
  119. }
  120. public String[] getArguments() {
  121. return argv;
  122. }
  123. public void setArguments(final String[] argv) {
  124. this.argv = argv;
  125. }
  126. /**
  127. * Parses the command line argument, injecting parsed values into fields.
  128. * <p>
  129. * This method must be explicitly invoked to cause a parse.
  130. *
  131. * @throws UnloggedFailure
  132. * if the command line arguments were invalid.
  133. * @see Option
  134. * @see Argument
  135. */
  136. protected void parseCommandLine() throws UnloggedFailure {
  137. parseCommandLine(this);
  138. }
  139. /**
  140. * Parses the command line argument, injecting parsed values into fields.
  141. * <p>
  142. * This method must be explicitly invoked to cause a parse.
  143. *
  144. * @param options
  145. * object whose fields declare Option and Argument annotations to
  146. * describe the parameters of the command. Usually {@code this}.
  147. * @throws UnloggedFailure
  148. * if the command line arguments were invalid.
  149. * @see Option
  150. * @see Argument
  151. */
  152. protected void parseCommandLine(Object options) throws UnloggedFailure {
  153. final CmdLineParser clp = newCmdLineParser(options);
  154. try {
  155. clp.parseArgument(argv);
  156. } catch (IllegalArgumentException err) {
  157. if (!clp.wasHelpRequestedByOption()) {
  158. throw new UnloggedFailure(1, "fatal: " + err.getMessage());
  159. }
  160. } catch (CmdLineException err) {
  161. if (!clp.wasHelpRequestedByOption()) {
  162. throw new UnloggedFailure(1, "fatal: " + err.getMessage());
  163. }
  164. }
  165. if (clp.wasHelpRequestedByOption()) {
  166. CommandMetaData meta = getClass().getAnnotation(CommandMetaData.class);
  167. String title = meta.name().toUpperCase() + ": " + meta.description();
  168. String b = com.gitblit.utils.StringUtils.leftPad("", title.length() + 2, '═');
  169. StringWriter msg = new StringWriter();
  170. msg.write('\n');
  171. msg.write(b);
  172. msg.write('\n');
  173. msg.write(' ');
  174. msg.write(title);
  175. msg.write('\n');
  176. msg.write(b);
  177. msg.write("\n\n");
  178. msg.write("USAGE\n");
  179. msg.write("─────\n");
  180. msg.write(' ');
  181. msg.write(commandName);
  182. msg.write('\n');
  183. msg.write(" ");
  184. clp.printSingleLineUsage(msg, null);
  185. msg.write("\n\n");
  186. String txt = getUsageText();
  187. if (!StringUtils.isEmpty(txt)) {
  188. msg.write(txt);
  189. msg.write("\n\n");
  190. }
  191. msg.write("ARGUMENTS & OPTIONS\n");
  192. msg.write("───────────────────\n");
  193. clp.printUsage(msg, null);
  194. msg.write('\n');
  195. String examples = usage().trim();
  196. if (!StringUtils.isEmpty(examples)) {
  197. msg.write('\n');
  198. msg.write("EXAMPLES\n");
  199. msg.write("────────\n");
  200. msg.write(examples);
  201. msg.write('\n');
  202. }
  203. throw new UnloggedFailure(1, msg.toString());
  204. }
  205. }
  206. /** Construct a new parser for this command's received command line. */
  207. protected CmdLineParser newCmdLineParser(Object options) {
  208. return new CmdLineParser(options);
  209. }
  210. public String usage() {
  211. Class<? extends BaseCommand> clazz = getClass();
  212. if (clazz.isAnnotationPresent(UsageExamples.class)) {
  213. return examples(clazz.getAnnotation(UsageExamples.class).examples());
  214. } else if (clazz.isAnnotationPresent(UsageExample.class)) {
  215. return examples(clazz.getAnnotation(UsageExample.class));
  216. }
  217. return "";
  218. }
  219. protected String getUsageText() {
  220. return "";
  221. }
  222. protected String examples(UsageExample... examples) {
  223. int sshPort = getContext().getGitblit().getSettings().getInteger(Keys.git.sshPort, 29418);
  224. String username = getContext().getClient().getUsername();
  225. String hostname = "localhost";
  226. String ssh = String.format("ssh -l %s -p %d %s", username, sshPort, hostname);
  227. StringBuilder sb = new StringBuilder();
  228. for (UsageExample example : examples) {
  229. sb.append(example.description()).append("\n\n");
  230. String syntax = example.syntax();
  231. syntax = syntax.replace("${ssh}", ssh);
  232. syntax = syntax.replace("${username}", username);
  233. syntax = syntax.replace("${cmd}", commandName);
  234. sb.append(" ").append(syntax).append("\n\n");
  235. }
  236. return sb.toString();
  237. }
  238. protected void showHelp() throws UnloggedFailure {
  239. argv = new String [] { "--help" };
  240. parseCommandLine();
  241. }
  242. private final class TaskThunk implements CancelableRunnable {
  243. private final CommandRunnable thunk;
  244. private final String taskName;
  245. private TaskThunk(final CommandRunnable thunk) {
  246. this.thunk = thunk;
  247. StringBuilder m = new StringBuilder();
  248. m.append(ctx.getCommandLine());
  249. this.taskName = m.toString();
  250. }
  251. @Override
  252. public void cancel() {
  253. synchronized (this) {
  254. try {
  255. onExit(STATUS_CANCEL);
  256. } finally {
  257. ctx = null;
  258. }
  259. }
  260. }
  261. @Override
  262. public void run() {
  263. synchronized (this) {
  264. final Thread thisThread = Thread.currentThread();
  265. final String thisName = thisThread.getName();
  266. int rc = 0;
  267. try {
  268. thisThread.setName("SSH " + taskName);
  269. thunk.run();
  270. out.flush();
  271. err.flush();
  272. } catch (Throwable e) {
  273. try {
  274. out.flush();
  275. } catch (Throwable e2) {
  276. }
  277. try {
  278. err.flush();
  279. } catch (Throwable e2) {
  280. }
  281. rc = handleError(e);
  282. } finally {
  283. try {
  284. onExit(rc);
  285. } finally {
  286. thisThread.setName(thisName);
  287. }
  288. }
  289. }
  290. }
  291. @Override
  292. public String toString() {
  293. return taskName;
  294. }
  295. }
  296. /** Runnable function which can throw an exception. */
  297. public static interface CommandRunnable {
  298. public void run() throws Exception;
  299. }
  300. /** Runnable function which can retrieve a project name related to the task */
  301. public static interface RepositoryCommandRunnable extends CommandRunnable {
  302. public String getRepository();
  303. }
  304. /**
  305. * Spawn a function into its own thread.
  306. * <p>
  307. * Typically this should be invoked within
  308. * {@link Command#start(Environment)}, such as:
  309. *
  310. * <pre>
  311. * startThread(new Runnable() {
  312. * public void run() {
  313. * runImp();
  314. * }
  315. * });
  316. * </pre>
  317. *
  318. * @param thunk
  319. * the runnable to execute on the thread, performing the
  320. * command's logic.
  321. */
  322. protected void startThread(final Runnable thunk) {
  323. startThread(new CommandRunnable() {
  324. @Override
  325. public void run() throws Exception {
  326. thunk.run();
  327. }
  328. });
  329. }
  330. /**
  331. * Terminate this command and return a result code to the remote client.
  332. * <p>
  333. * Commands should invoke this at most once.
  334. *
  335. * @param rc exit code for the remote client.
  336. */
  337. protected void onExit(final int rc) {
  338. exit.onExit(rc);
  339. }
  340. private int handleError(final Throwable e) {
  341. if ((e.getClass() == IOException.class && "Pipe closed".equals(e.getMessage())) || //
  342. (e.getClass() == SshException.class && "Already closed".equals(e.getMessage())) || //
  343. e.getClass() == InterruptedIOException.class) {
  344. // This is sshd telling us the client just dropped off while
  345. // we were waiting for a read or a write to complete. Either
  346. // way its not really a fatal error. Don't log it.
  347. //
  348. return 127;
  349. }
  350. if (e instanceof UnloggedFailure) {
  351. } else {
  352. final StringBuilder m = new StringBuilder();
  353. m.append("Internal server error");
  354. String user = ctx.getClient().getUsername();
  355. if (user != null) {
  356. m.append(" (user ");
  357. m.append(user);
  358. m.append(")");
  359. }
  360. m.append(" during ");
  361. m.append(ctx.getCommandLine());
  362. log.error(m.toString(), e);
  363. }
  364. if (e instanceof Failure) {
  365. final Failure f = (Failure) e;
  366. try {
  367. err.write((f.getMessage() + "\n").getBytes(Charsets.UTF_8));
  368. err.flush();
  369. } catch (IOException e2) {
  370. } catch (Throwable e2) {
  371. log.warn("Cannot send failure message to client", e2);
  372. }
  373. return f.exitCode;
  374. } else {
  375. try {
  376. err.write("fatal: internal server error\n".getBytes(Charsets.UTF_8));
  377. err.flush();
  378. } catch (IOException e2) {
  379. } catch (Throwable e2) {
  380. log.warn("Cannot send internal server error message to client", e2);
  381. }
  382. return 128;
  383. }
  384. }
  385. /**
  386. * Spawn a function into its own thread.
  387. * <p>
  388. * Typically this should be invoked within
  389. * {@link Command#start(Environment)}, such as:
  390. *
  391. * <pre>
  392. * startThread(new CommandRunnable() {
  393. * public void run() throws Exception {
  394. * runImp();
  395. * }
  396. * });
  397. * </pre>
  398. * <p>
  399. * If the function throws an exception, it is translated to a simple message
  400. * for the client, a non-zero exit code, and the stack trace is logged.
  401. *
  402. * @param thunk
  403. * the runnable to execute on the thread, performing the
  404. * command's logic.
  405. */
  406. protected void startThread(final CommandRunnable thunk) {
  407. final TaskThunk tt = new TaskThunk(thunk);
  408. task.set(executor.submit(tt));
  409. }
  410. /** Thrown from {@link CommandRunnable#run()} with client message and code. */
  411. public static class Failure extends Exception {
  412. private static final long serialVersionUID = 1L;
  413. final int exitCode;
  414. /**
  415. * Create a new failure.
  416. *
  417. * @param exitCode
  418. * exit code to return the client, which indicates the
  419. * failure status of this command. Should be between 1 and
  420. * 255, inclusive.
  421. * @param msg
  422. * message to also send to the client's stderr.
  423. */
  424. public Failure(final int exitCode, final String msg) {
  425. this(exitCode, msg, null);
  426. }
  427. /**
  428. * Create a new failure.
  429. *
  430. * @param exitCode
  431. * exit code to return the client, which indicates the
  432. * failure status of this command. Should be between 1 and
  433. * 255, inclusive.
  434. * @param msg
  435. * message to also send to the client's stderr.
  436. * @param why
  437. * stack trace to include in the server's log, but is not
  438. * sent to the client's stderr.
  439. */
  440. public Failure(final int exitCode, final String msg, final Throwable why) {
  441. super(msg, why);
  442. this.exitCode = exitCode;
  443. }
  444. }
  445. /** Thrown from {@link CommandRunnable#run()} with client message and code. */
  446. public static class UnloggedFailure extends Failure {
  447. private static final long serialVersionUID = 1L;
  448. /**
  449. * Create a new failure.
  450. *
  451. * @param msg
  452. * message to also send to the client's stderr.
  453. */
  454. public UnloggedFailure(final String msg) {
  455. this(1, msg);
  456. }
  457. /**
  458. * Create a new failure.
  459. *
  460. * @param exitCode
  461. * exit code to return the client, which indicates the
  462. * failure status of this command. Should be between 1 and
  463. * 255, inclusive.
  464. * @param msg
  465. * message to also send to the client's stderr.
  466. */
  467. public UnloggedFailure(final int exitCode, final String msg) {
  468. this(exitCode, msg, null);
  469. }
  470. /**
  471. * Create a new failure.
  472. *
  473. * @param exitCode
  474. * exit code to return the client, which indicates the
  475. * failure status of this command. Should be between 1 and
  476. * 255, inclusive.
  477. * @param msg
  478. * message to also send to the client's stderr.
  479. * @param why
  480. * stack trace to include in the server's log, but is not
  481. * sent to the client's stderr.
  482. */
  483. public UnloggedFailure(final int exitCode, final String msg, final Throwable why) {
  484. super(exitCode, msg, why);
  485. }
  486. }
  487. }