The Unison-ssh utility is for users of the Unison file synchronizer under Windows. The thing is, Unison needs SSH and that isn't so straightforward under Windows. Many people instead use the almost-equivalent Plink and then have some kind of wrapper around it. Unison-ssh is a particularly convenient wrapper.
ssh username@hostname
for the
host you're synchronizing with. This will sort out keys and assure
you that it's working. Read the unison docs for an explanation.
Unison-ssh is a wrapper around plink with some nice features. (1) It's a drop-in replacement from the perspective of Unison, so you can just use the standard unison commands without any messing about. (2) It lets you type in your SSH password interactively if you want, rather than forcing you to run Pagent. Also, it's invisible when you type it in interactively, unlike most other unison/plink wrappers.
Notes. If you already have an ssh.exe on your system, then it probably works fine already and you don't need unison-ssh at all. Unison-ssh is merely a wrapper around Plink. If you already have plink installed somewhere on your path, then that will be used. If you don't, then unison-ssh contains a copy and will automatically install that copy in your windows directory.
This source code compiles with VS2005, BCB5 and g++. Also, the rest of this page shows the main source code.
#include <windows.h> #include <stdio.h> #include <string.h> #ifdef DEBUG void TRACE(const char *msg) {fprintf(stderr,"*** %s\n",msg);} void TRACE(const char *msg, char *buf, int len) {for (int i=0; i<len; i++) {if (buf[i]<32) buf[i]='.';} buf[len]=0; fprintf(stderr,"*** %s [[[%s]]]\n",msg,buf);} #else inline void TRACE(const char *) {} inline void TRACE(const char *, char *, int) {} #endif // UNISON-SSH, by Lucian Wischik 2005. // This program is to help out the windows version of Unison, the file synchronisation tool. // Unison assumes a command "ssh" has been installed which works in a particular way. // The closest thing under Windows is "plink", but it's not quite the same. // Hence this program UNISON-SSH, a wrapper around plink, which makes it act enough // like "ssh" for Unison to be happy without needing any further workarounds. // (1) Unison expects to find a program called "ssh", but plink has a different name, // so UNISON-SSH is called "ssh" // (2) Unison uses the "-e none" argument, which plink lacks, // so UNISON-SSH gobbles up that argument before invoking plink // (3) Unison expects ssh not to emit any output of its own, but plink sometimes // prompts the user for the password, so UNISON-SSH manages the password // prompt on its own without feeding it through. // (4) Unison needs plink installed, but it's unpleasant to ask the user to install // too many different things, so UNISON-SSH will itself install plink into // the windows directory if it couldn't already be found on the path. // Here's more detail on how UNISON-SSH does it: // // (2) To gobble up the "-e none" argument, we parse the command line manually. // We parse it as a single long text string, instead of using argc/argv. Why? // because argc/argv have already lost information from the command line string // (e.g. how many spaces there were, whether items had quotation marks around them) // which it seemed unreasonable to lose. So we don't use argc/argv. // // (3) To manage the password, we run plink.exe with redirected input/output handles: // stdin -("inpump")-> hinwrite--pipe1-->hinread [plink] houtwrite--pipe2-->houtread -("outpump")-> stdout // The two pumps ("inpump") and ("output") are run as their own threads. [plink] is run // as a process. The inpump mainly forwards stuff straight through. One effect is so // that we know when stdin ends. As for the output, if it encounters the string // "Password: " in the first 1000 bytes of output, it displays a message to CONOUT$ // (nb. not the STDOUT, which has been redirected by unison). If we'd run SSH with console // input, then now we leave the inpump to manage the password and send it // via hinwrite to [plink]. Note that plink requires the entire password to be submitted // in one go, not character-by-character. But if we'd redirected stdin, then we // ask for the password from CONIN$ rather than stdin, and this asking is instead // done in the outpump. // Note: if the user has "pagent" installed, then the Password: prompt is bypassed // and none of functionality is invoked. But the rest of it still works fine. // // (4) To install plink if necessary, we carry it as a binary payload. If our attempt // to CreateProcess("plink.exe ...") fails, then we'll dump our payload into the windows // directory and try again. // // (5) During the execution of this program, if it has been run without redirected stdin, // then right from the start we set console input to "character-by-character" // and we "disable-echo". For the password prompt, this means that the password isn't // displayed onscreen as the user types it. For the main execution, it means that // keypresses get sent to the remote machine as soon as they happen, and the remote // machine does the echoing job. But if stdin had been redirected, then the only // time we set console mode is to "line-by-line/disable-echo" immediately prior to // asking for the password. // // (6) Sometimes when you run ssh you're doing it with a command that terminates (eg. "ls"). // In these cases, the plink process will terminate naturally, and we go on to close // our input handles and output handles, and so the inpump and outpump come to a natural end, // and the program finishes. // Sometimes you're doing it with a command that goes on forever (eg. "unison -server"). // Presumably the user will close the input stream when they want it to end, or press ctrl-c. // If they close the input stream then the inpump will come to an end. If this should happen // before plink has finished, then we TerminateProcess(plink) to kill it. I'm worried that // this is too abrubt, but I don't think there are any other options. HANDLE hstdin=INVALID_HANDLE_VALUE; HANDLE hstdout=INVALID_HANDLE_VALUE; HANDLE hinwrite=INVALID_HANDLE_VALUE; HANDLE hinread=INVALID_HANDLE_VALUE; HANDLE houtread=INVALID_HANDLE_VALUE; HANDLE houtwrite=INVALID_HANDLE_VALUE; HANDLE hinthread=0; HANDLE houtthread=0; bool stdin_is_console=false; // All the above are initialized in main() before any threads are created. // The following is set by outpump-thread upon encountering "Password:". // It is read (and reset) by inpump-thread. volatile bool inpump_passwording=false; DWORD WINAPI inpump(void *) { // We'll use the same buffer both for normal inpumping, // and also for accumulating the password as the user types it in (if we're passwording). // For this second purposes, index "i" says how much of the buffer we've // filled with that password. DWORD red,writ; BOOL res; char buf[1026]; int off=0; for (;;) { TRACE("inpump ReadFile"); res=ReadFile(hstdin,buf+off,1024-off,&red,0); if (!res || red==0) break; if (!inpump_passwording) { WriteFile(hinwrite,buf+off,red,&writ,0); TRACE("inpump WriteFile",buf,red); continue; } // can write directly off+=red; red=off; if (buf[red-1]!='\r' && buf[red-1]!='\n' && red<1000) continue; // plink expects the password to be \r\n terminated. Note that // the raw ReadFile might or might not have terminated it properly. if (buf[red-1]=='\r' || buf[red-1]=='\n') red--; if (buf[red-1]=='\r' || buf[red-1]=='\n') red--; buf[red]='\r'; buf[red+1]='\n'; red+=2; WriteFile(hinwrite,buf,red,&writ,0); TRACE("inpump submitting password ",buf,red); inpump_passwording=false; off=0; // Now print an empty line so it looks good to the user HANDLE hconout=CreateFile("CONOUT$",GENERIC_READ|GENERIC_WRITE,FILE_SHARE_WRITE,0,OPEN_EXISTING,0,0); if (hconout!=INVALID_HANDLE_VALUE) {WriteFile(hconout,"\r\n",2,&writ,0); CloseHandle(hconout);} } TRACE("inpump done."); return 0; } DWORD WINAPI outpump(void *) { DWORD Countup=0; // when countup is above 1000, we won't look for Password: prompt any more. bool IgnoreComingCRLF=false; // if ever we submit a password then plink will generate a spurious crlf char buf[1024]; DWORD red,writ; BOOL res; // for (;;) { TRACE(IgnoreComingCRLF ? "outpump ReadFile IgnoreCRLF" : "outpump ReadFile"); res=ReadFile(houtread,buf,1024,&red,0); if (!res || red==0) break; bool wasignore=IgnoreComingCRLF; IgnoreComingCRLF=false; if (wasignore && red>=2 && buf[0]=='\r' && buf[1]=='\n') {red-=2; if (red==0) continue; memcpy(&buf[0],&buf[2],red);} if (Countup>1000 || red<9 || red>10 || strncmp(buf,"Password:",9)!=0 || (red==10 && buf[9]!=' ')) { WriteFile(hstdout,buf,red,&writ,0); TRACE("outpump WriteFile",buf,red); Countup+=red; continue; } // otherwise, plink has just asked us for the password... So tell the user. HANDLE hconout=CreateFile("CONOUT$",GENERIC_READ|GENERIC_WRITE,FILE_SHARE_WRITE,0,OPEN_EXISTING,0,0); if (hconout!=INVALID_HANDLE_VALUE) WriteFile(hconout,"Password: ",10,&writ,0); // and obtain the password if (stdin_is_console) { // If at a console, the input-pump can manage getting+sending the password. We'll no longer watch inpump_passwording=true; Countup=1000; IgnoreComingCRLF=true; if (hconout!=INVALID_HANDLE_VALUE) CloseHandle(hconout); continue; } // Otherwise we'll grab the console, set it to secrecy, obtain the password. // Note: we don't bother freezing the inpump. If it had already got data, that would // have messed up password entry, so there's no sense trying to fight it. const char *err=0; HANDLE hconin=INVALID_HANDLE_VALUE; BOOL res=TRUE; DWORD mode=0; if (err==0) {hconin=CreateFile("CONIN$",GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ,0,OPEN_EXISTING,0,0); if (hconin==INVALID_HANDLE_VALUE) err="Unable to read from console";} if (err==0) {res=GetConsoleMode(hconin,&mode); if (!res) err="Unable to get console input mode";} if (err==0) {res=SetConsoleMode(hconin,ENABLE_LINE_INPUT); if (!res) err="Unable to set console input mode";} if (err==0) { res=ReadFile(hconin,buf,1021,&red,0); if (res && red>2) { if (buf[red-1]=='\r' || buf[red-1]=='\n') red--; if (buf[red-1]=='\r' || buf[red-1]=='\n') red--; buf[red]='\r'; buf[red+1]='\n'; buf[red+2]=0; red+=2; WriteFile(hinwrite,buf,red,&writ,0); TRACE("outpump submitted password",buf,red); if (hconout!=INVALID_HANDLE_VALUE) WriteFile(hconout,"\r\n",2,&writ,0); } } else { void *msg; FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM,NULL,GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&msg, 0,NULL); fprintf(stderr,"%s- %s\n",err,msg); LocalFree(msg); } if (mode!=0) SetConsoleMode(hconin,mode); if (hconin!=INVALID_HANDLE_VALUE) CloseHandle(hconin); if (hconout!=INVALID_HANDLE_VALUE) CloseHandle(hconout); Countup=1000; IgnoreComingCRLF=true; if (!res || red==0) break; // in case the user aborted at the password prompt } TRACE("outpump done."); return 0; } int main() { const char *cmd1 = GetCommandLine(); TRACE(cmd1); char *cmd2 = new char[strlen(cmd1)+16384]; const char *src=cmd1; char *dst=cmd2; // get past initial command if (*src=='\"') {src++; while (*src!='\"' && *src!=0) src++; if (*src=='\"') src++;} else {while (*src!=' ' && *src!=0) src++;} while (*src==' ') src++; dst[0]='p'; dst[1]='l'; dst[2]='i'; dst[3]='n'; dst[4]='k'; dst[5]=' '; dst+=6; // copy the rest, but skipping out "-e arg" bool blownit=false; while (*src!=0) { if (src[0]=='-' && src[1]=='e' && src[2]==' ' && !blownit) { src+=3; while (*src==' ') src++; if (*src=='\"') {src++; while (*src!='\"' && *src!=0) src++; if (*src=='\"') src++;} else {while (*src!=' ' && *src!=0) src++;} while (*src==' ') src++; blownit=true; continue; } if (*src=='\"') { *dst=*src; dst++; src++; while (*src!='\"' && *src!=0) {*dst=*src; dst++; src++;} if (*src=='\"') {*dst=*src; dst++; src++;} continue; } while (*src!=' ' && *src!=0) { *dst=*src; dst++; src++; continue; } while (*src==' ') { *dst=*src; dst++; src++; continue; } } *dst=0; BOOL res=FALSE; const char* err=0; DWORD tid; DWORD ret=1; DWORD red; HANDLE hconin=INVALID_HANDLE_VALUE; DWORD mode=0; SECURITY_ATTRIBUTES sa; ZeroMemory(&sa,sizeof(sa)); sa.nLength=sizeof(sa); sa.bInheritHandle=TRUE; sa.lpSecurityDescriptor=0; if (err==0) {hstdin=GetStdHandle(STD_INPUT_HANDLE); if (hstdin==INVALID_HANDLE_VALUE) err="Failed to open stdin";} if (err==0) {hstdout=GetStdHandle(STD_OUTPUT_HANDLE); if (hstdout==INVALID_HANDLE_VALUE) err="Failed to open stdout";} // if (err==0) stdin_is_console=(PeekConsoleInput(hstdin,0,0,&red) != 0); // http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/dnaraskdr/html/askgui07152003.asp if (err==0 && stdin_is_console) {hconin=CreateFile("CONIN$",GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ,0,OPEN_EXISTING,0,0); if (hconin==INVALID_HANDLE_VALUE) err="Unable to setup read from console";} if (err==0 && stdin_is_console) {res=GetConsoleMode(hconin,&mode); if (!res) err="Unable to setup get console input mode";} if (err==0 && stdin_is_console) {res=SetConsoleMode(hconin,mode&~(ENABLE_ECHO_INPUT|ENABLE_LINE_INPUT)); if (!res) err="Unable to setup console input mode";} if (hconin!=INVALID_HANDLE_VALUE) CloseHandle(hconin); hconin=INVALID_HANDLE_VALUE; // if (err==0) {res=CreatePipe(&hinread,&hinwrite,&sa,0); if (!res) err="Failed to create input pipe";} if (err==0) {res=CreatePipe(&houtread,&houtwrite,&sa,0); if (!res) err="Failed to create output pipe";} if (err==0) {houtthread=CreateThread(0,0,outpump,0,0,&tid); if (houtthread==0) err="Failed to create output thread";} if (err==0) {hinthread=CreateThread(0,0,inpump,0,0,&tid); if (hinthread==0) err="Failed to create input thread";} STARTUPINFO si; ZeroMemory(&si,sizeof(si)); si.cb=sizeof(si); si.dwFlags=STARTF_USESTDHANDLES; si.hStdInput=hinread; si.hStdOutput=houtwrite; si.hStdError=GetStdHandle(STD_ERROR_HANDLE); PROCESS_INFORMATION pi; ZeroMemory(&pi,sizeof(pi)); if (err==0) res=CreateProcess(0,cmd2,0,0,TRUE,0,0,0,&si,&pi); if (err==0 && !res && GetLastError()==ERROR_FILE_NOT_FOUND) { char fn[MAX_PATH+12]; GetWindowsDirectory(fn,MAX_PATH); strcat(fn,"\\plink.exe"); DWORD attr = GetFileAttributes(fn); if (attr==0xFFFFFFFF) { HRSRC hrsrc=0; HGLOBAL hglob=0; void *buf=0; HANDLE hf=INVALID_HANDLE_VALUE; DWORD size=0; if (err==0) {hrsrc=FindResource(GetModuleHandle(0),MAKEINTRESOURCE(1),RT_RCDATA); if (hrsrc==0) err="Ssh wasn't built with plink.exe resource";} if (err==0) {hglob=LoadResource(GetModuleHandle(0),hrsrc); if (hglob==0) err="Failed to load plink.exe resource";} if (err==0) {buf=LockResource(hglob); if (buf==0) err="Failed to lock plink.exe resource";} if (err==0) {size=SizeofResource(GetModuleHandle(0),hrsrc); if (size==0) err="Failed to size plink.exe resource";} if (err==0) {hf=CreateFile(fn,GENERIC_WRITE,FILE_SHARE_WRITE,0,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0); if (hf==INVALID_HANDLE_VALUE) err="Unable to create plink.exe in windows directory";} if (err==0) {DWORD writ; res=WriteFile(hf,buf,size,&writ,0); if (!res || writ!=size) err="Failed to properly write plink.exe in windows directory";} if (hf!=INVALID_HANDLE_VALUE) CloseHandle(hf); if (err==0) res=CreateProcess(0,cmd2,0,0,TRUE,0,0,0,&si,&pi); } } if (err==0 && !res) err="Failed to launch plink.exe"; if (err==0) { TRACE("waiting for plink..."); // Some remote commands terminate themselves. Others run on for ever. // To handle these we respond to a closure of the input stream by killing plink: HANDLE h[2]; h[0]=hinthread; h[1]=pi.hProcess; DWORD r = WaitForMultipleObjects(2,h,FALSE,INFINITE); if (r==0) { TRACE("stdin closed; terminating plink!"); TerminateProcess(pi.hProcess,1); WaitForSingleObject(pi.hProcess,INFINITE); } GetExitCodeProcess(pi.hProcess,&ret); TRACE("plink has exited."); } else { void *msg; FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM,NULL,GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&msg, 0,NULL); fprintf(stderr,"%s- %s\n",err,msg); LocalFree(msg); ret=1; } // Note: the order of closing stuff here is critical to avoid deadlock. // ... // by closing hstdin, the inpump's ReadFile will abort TRACE("closing hstdin"); if (hstdin!=INVALID_HANDLE_VALUE) CloseHandle(hstdin); TRACE("closing hinwrite"); if (hinwrite!=INVALID_HANDLE_VALUE) CloseHandle(hinwrite); TRACE("closing hinread"); if (hinread!=INVALID_HANDLE_VALUE) CloseHandle(hinread); // For the output, I forget the reason for this order. // We're clearly closing it from the right hand side to the left. TRACE("closing hstdout"); if (hstdout!=INVALID_HANDLE_VALUE) CloseHandle(hstdout); TRACE("closing houtwrite"); if (houtwrite!=INVALID_HANDLE_VALUE) CloseHandle(houtwrite); TRACE("closing houtread"); if (houtread!=INVALID_HANDLE_VALUE) CloseHandle(houtread); // The above things guarantee that the threads are unblocked TRACE("closing hinthread"); if (hinthread!=0) {WaitForSingleObject(hinthread,INFINITE); CloseHandle(hinthread);} TRACE("closing houtthread"); if (houtthread!=0) {WaitForSingleObject(houtthread,INFINITE); CloseHandle(houtthread);} // We have already waited for the plink process to terminate, above. TRACE("closing plink handles"); if (pi.hThread!=0) CloseHandle(pi.hThread); if (pi.hProcess!=0) CloseHandle(pi.hProcess); if (mode!=0) { TRACE("restoring console mode"); hconin=CreateFile("CONIN$",GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ,0,OPEN_EXISTING,0,0); if (hconin!=INVALID_HANDLE_VALUE) {SetConsoleMode(hconin,mode); CloseHandle(hconin);} } if (cmd2!=0) delete[] cmd2; TRACE("ssh finished."); return ret; }