/*
**	Eject - AmigaDOS 2.04 utility
**
**	Copyright © 1994-1994 by Olaf `Olsen' Barthel
**		Public Domain
**
**	:ts=4
*/

	/* Just a few includes... */

#include <intuition/intuitionbase.h>

#include <exec/execbase.h>
#include <exec/memory.h>
#include <exec/io.h>

#include <devices/scsidisk.h>

#include <workbench/workbench.h>
#include <workbench/startup.h>

#include <dos/filehandler.h>
#include <dos/dosextens.h>
#include <dos/rdargs.h>

#include <clib/intuition_protos.h>
#include <clib/exec_protos.h>
#include <clib/icon_protos.h>
#include <clib/dos_protos.h>
#include <clib/wb_protos.h>

#include <string.h>

	/* SCSI start/stop unit command. */

struct StartStopUnit
{
	UBYTE	OperationCode,		/* 0x1B */
			LUN_Reserved_Immed,	/* %AAABBBBC, A = LUN, C = Immediate (0 = wait for completion) */
			Reserved[2],
			LoEj_Start,			/* %000000AB, A = Load/Eject (1 = eject after stop), */
			Control;			/* B = Start/Stop (1 = Start, 0 = Stop) */
};

	/* SCSI test unit ready command. */

struct TestUnitReady
{
	UBYTE	Command,			/* 0x00 */
			LUN,
			Reserved[3],
			Control;
};

	/* SCSI inquiry command. */

struct Inquiry
{
	UBYTE	Command,		/* 0x12 */
			LUN,
			PageCode,
			Reserved,
			AllocationLength,
			Control;
};

	/* Data format returned by inquiry command. */

struct InquiryData
{
	UBYTE	PeripheralType,
			DeviceTypeModifier,
			Version,
			Reserved[5],
			Vendor[8],
			Product[16],
			Revision[4];
};

	/* Version identification. */

STRPTR					 Version = "$VER: Eject 1.0 (25.12.94)\r\n";

	/* Global library bases. */

struct ExecBase			*SysBase;
struct DosLibrary		*DOSBase;
struct IntuitionBase	*IntuitionBase;
struct Library			*IconBase,
						*WorkbenchBase;

	/* Shell command template data. */

STRPTR Template = "DEVICE/A/M";

enum { ARG_DEVICE=0, ARGCOUNT };

	/* Prototypes for this module. */

BYTE	DoSCSICmd(struct IOStdReq *DeviceRequest,ULONG Unit,APTR Command,LONG CommandLen,APTR Data,LONG DataLen);
BYTE	TestUnitReady(struct IOStdReq *DeviceRequest,ULONG Unit);
BOOL	CheckMediaType(struct IOStdReq *DeviceRequest,ULONG Unit);
BYTE	EjectMedia(struct IOStdReq *DeviceRequest,ULONG Unit);
BOOL	Eject(const STRPTR DeviceName,ULONG Unit,ULONG Flags,BOOL Echo);
BOOL	FindDevice(const BPTR FileLock,const STRPTR FileName,STRPTR Device,ULONG *Unit,ULONG *Flags);

	/* Main(VOID):
	 *
	 *	Program entry point.
	 */

LONG __saveds
Main(VOID)
{
	struct Process		*ThisProcess;
	LONG				 Result = RETURN_FAIL;
	struct WBStartup	*WBenchMsg;

		/* Set up SysBase. */

	SysBase = *(struct ExecBase **)4;

		/* That's me. */

	ThisProcess = (struct Process *)FindTask(NULL);

		/* Are we a child of Workbench? */

	if(ThisProcess -> pr_CLI)
		WBenchMsg = NULL;
	else
	{
			/* Wait for the startup message. */

		WaitPort(&ThisProcess -> pr_MsgPort);

		WBenchMsg = (struct WBStartup *)GetMsg(&ThisProcess -> pr_MsgPort);
	}

		/* Open dos.library. */

	if(DOSBase = (struct DosLibrary *)OpenLibrary("dos.library",37))
	{
			/* Workbench invocation? */

		if(WBenchMsg)
		{
			struct MsgPort *IconPort;

				/* Hold it... */

			Forbid();

				/* See if Eject is already running, if so tell
				 * it to quit.
				 */

			if(IconPort = FindPort("« Eject »"))
				Signal(IconPort -> mp_SigTask,SIGBREAKF_CTRL_C);

			Permit();

				/* Are we the one and only ?*/

			if(!IconPort)
			{
					/* Create the global port. */

				if(IconPort = CreateMsgPort())
				{
						/* Give it a name and a special priority. */

					IconPort -> mp_Node . ln_Name	= "« Eject »";
					IconPort -> mp_Node . ln_Pri	= 1;

						/* Open Intuition if possible. */

					IntuitionBase = (struct IntuitionBase *)OpenLibrary("intuition.library",37);

						/* Take care of the remaining libraries. */

					if(IconBase = OpenLibrary("icon.library",37))
					{
						if(WorkbenchBase = OpenLibrary("workbench.library",37))
						{
							BPTR				 OldDir = CurrentDir(WBenchMsg -> sm_ArgList -> wa_Lock);
							struct DiskObject	*Icon = GetDiskObjectNew(WBenchMsg -> sm_ArgList -> wa_Name);

								/* We need the icon. */

							if(Icon)
							{
								struct AppIcon *AppIcon;

									/* Add the AppIcon. */

								if(AppIcon = AddAppIconA(0,0,FilePart(WBenchMsg -> sm_ArgList -> wa_Name),IconPort,NULL,Icon,NULL))
								{
									struct AppMessage	*Message;
									ULONG				 Signals;
									BOOL				 Done = FALSE;
									struct Window		*ReqWindow = NULL;
									struct AppWindow	*AppWindow = NULL;

										/* Make the global port visible. */

									AddPort(IconPort);

										/* Keep going... */

									do
									{
											/* Tie in the window signal if necessary. */

										if(ReqWindow)
											Signals = 1L << ReqWindow -> UserPort -> mp_SigBit;
										else
											Signals = NULL;

											/* Wait for something to happen. */

										Signals = Wait(Signals | (1L << IconPort -> mp_SigBit) | SIGBREAKF_CTRL_C);

											/* Do we have the notification window open? */

										if(ReqWindow)
										{
												/* Did the signal originate here? */

											if(Signals & (1L << ReqWindow -> UserPort -> mp_SigBit))
											{
												ULONG	IDCMP = NULL;
												LONG	Result;

													/* Process the events. */

												Result = SysReqHandler(ReqWindow,&IDCMP,FALSE);

													/* Shut the window? */

												if(Result == 0 || (Result == -2 && !(IDCMP & IDCMP_RAWKEY)))
												{
													if(AppWindow)
													{
														RemoveAppWindow(AppWindow);

														AppWindow = NULL;
													}

													FreeSysRequest(ReqWindow);

													ReqWindow = NULL;
												}
											}
										}

											/* A signal from Workbench? */

										if(Signals & (1L << IconPort -> mp_SigBit))
										{
												/* Pick up all incoming data. */

											while(Message = (struct AppMessage *)GetMsg(IconPort))
											{
													/* User dropped icons on the AppWindow/AppIcon. */

												if(Message -> am_NumArgs)
												{
													UBYTE	DeviceName[256];
													ULONG	Unit,Flags;
													LONG	i;

														/* Take care of all entries. */

													for(i = 0 ; i < Message -> am_NumArgs ; i++)
													{
														if(FindDevice(Message -> am_ArgList[i] . wa_Lock,Message -> am_ArgList[i] . wa_Name,DeviceName,&Unit,&Flags))
															Eject(DeviceName,Unit,Flags,FALSE);
													}
												}
												else
												{
														/* It's just a double-click on the icon.
														 * See if the notification window is
														 * already open.
														 */

													if(ReqWindow)
													{
															/* Make the window visible. */

														if(IntuitionBase -> LibNode . lib_Version >= 39)
															ScreenPosition(ReqWindow -> WScreen,SPOS_MAKEVISIBLE,ReqWindow -> LeftEdge,ReqWindow -> TopEdge,ReqWindow -> LeftEdge + ReqWindow -> Width - 1,ReqWindow -> TopEdge + ReqWindow -> Height - 1);

														WindowToFront(ReqWindow);

														ScreenToFront(ReqWindow -> WScreen);

														ActivateWindow(ReqWindow);
													}
													else
													{
															/* Did we succeed in opening Intuition? */

														if(IntuitionBase)
														{
															struct EasyStruct Easy;

																/* Fill in the data. */

															Easy . es_StructSize	= sizeof(struct EasyStruct);
															Easy . es_Flags			= NULL;
															Easy . es_Title			= (STRPTR)"Eject";
															Easy . es_GadgetFormat	= (STRPTR)"Continue";
															Easy . es_TextFormat	= (STRPTR)"\"Eject\" Copyright © 1994-1995 by\nOlaf `Olsen' Barthel <olsen@sourcery.han.de>\nPublic Domain";

																/* Open the notification window and just
																 * for the fun of it make it an AppWindow.
																 */

															if(ReqWindow = BuildEasyRequest(NULL,&Easy,IDCMP_RAWKEY,NULL))
																AppWindow = AddAppWindowA(0,0,ReqWindow,IconPort,NULL);
														}
													}
												}

												ReplyMsg((struct Message *)Message);
											}
										}

											/* Terminate? */

										if(Signals & SIGBREAKF_CTRL_C)
											Done = TRUE;
									}
									while(!Done);

										/* Remove the AppWindow link. */

									if(AppWindow)
										RemoveAppWindow(AppWindow);

										/* Remove the AppIcon. */

									RemoveAppIcon(AppIcon);

										/* Hide the global port. */

									RemPort(IconPort);

										/* Dispatch all remaining messages. */

									while(Message = (struct AppMessage *)GetMsg(IconPort))
										ReplyMsg((struct Message *)Message);

										/* Close the notification window if necessary. */

									if(ReqWindow)
										FreeSysRequest(ReqWindow);
								}

									/* Get rid of the icon. */

								FreeDiskObject(Icon);
							}

								/* Get back... */

							CurrentDir(OldDir);

								/* And clean up the libraries. */

							CloseLibrary(WorkbenchBase);
						}

						CloseLibrary(IconBase);
					}

					if(IntuitionBase)
						CloseLibrary((struct Library *)IntuitionBase);

						/* Don't forget the global port. */

					DeleteMsgPort(IconPort);
				}
			}
		}
		else
		{
			struct RDArgs	*ArgsPtr;
			STRPTR			 Args[ARGCOUNT];

				/* Clear the args. */

			memset(Args,0,sizeof(Args));

				/* Read the command arguments if any. */

			if(ArgsPtr = ReadArgs(Template,(LONG *)Args,NULL))
			{
				STRPTR *Names = (STRPTR *)Args[ARG_DEVICE];

					/* Process all names. */

				while(*Names)
				{
					UBYTE	DeviceName[256];
					ULONG	Unit,Flags;
					BPTR	FileLock;

						/* Try to get a lock on the object in question. */

					FileLock = Lock(*Names,ACCESS_READ);

						/* Try to find the corresponding device. */

					if(FindDevice(FileLock,*Names,DeviceName,&Unit,&Flags))
					{
							/* Don't keep the icon longer on the
							 * desktop than necessary.
							 */

						UnLock(FileLock);

							/* Try to eject the media. */

						if(Eject(DeviceName,Unit,Flags,TRUE))
							Result = RETURN_OK;
						else
						{
							if(Result == RETURN_FAIL)
								Result = RETURN_ERROR;
							else
								Result = RETURN_WARN;
						}
					}
					else
					{
							/* Release the file lock. */

						UnLock(FileLock);

						Printf("Eject: Unable to find device \"%s\".\n",*Names);

						if(Result == RETURN_FAIL)
							Result = RETURN_ERROR;
						else
							Result = RETURN_WARN;
					}

						/* Proceed to the next name... */

					Names++;
				}
			}
			else
			{
				PrintFault(IoErr(),"Eject");

				Result = RETURN_ERROR;
			}
		}

			/* Clean up. */

		CloseLibrary((struct Library *)DOSBase);
	}
	else
		ThisProcess -> pr_Result2 = ERROR_INVALID_RESIDENT_LIBRARY;

		/* Reply the startup message if necessary. */

	if(WBenchMsg)
	{
		Forbid();

		ReplyMsg((struct Message *)WBenchMsg);
	}

	return(Result);
}

	/* DoSCSICmd(...):
	 *
	 *	Send a SCSI command to the driver in question.
	 */

BYTE
DoSCSICmd(struct IOStdReq *DeviceRequest,ULONG Unit,APTR Command,LONG CommandLen,APTR Data,LONG DataLen)
{
	struct SCSICmd	 SCSICmd;
	UBYTE			*CommandArray = Command,
					 SenseBuffer[20];

		/* Patch in the LUN. */

	CommandArray[1] |= ((Unit / 10) & 7) << 5;

		/* Set up the basic data. */

	DeviceRequest -> io_Command		= HD_SCSICMD;
	DeviceRequest -> io_Data		= &SCSICmd;
	DeviceRequest -> io_Length		= sizeof(struct SCSICmd);

		/* Now fill in the command. */

	memset(&SCSICmd,0,sizeof(struct SCSICmd));

	SCSICmd . scsi_Command		= Command;
	SCSICmd . scsi_CmdLength	= CommandLen;
	SCSICmd . scsi_Flags		= SCSIF_AUTOSENSE;
	SCSICmd . scsi_SenseData	= SenseBuffer;
	SCSICmd . scsi_SenseLength	= 18;

	if(Data)
	{
		SCSICmd . scsi_Data		= Data;
		SCSICmd . scsi_Length	= DataLen;

		SCSICmd . scsi_Flags |= SCSIF_READ;
	}
	else
		SCSICmd . scsi_Flags |= SCSIF_WRITE;

		/* Cast the dice. */

	return(DoIO((struct IORequest *)DeviceRequest));
}

	/* TestUnitReady(struct IOStdReq *DeviceRequest,ULONG Unit):
	 *
	 *	Check if this particular device is ready for action,
	 *	i.e. there is a medium loaded.
	 */

BYTE
TestUnitReady(struct IOStdReq *DeviceRequest,ULONG Unit)
{
	struct TestUnitReady TestUnitReady;

	memset(&TestUnitReady,0,sizeof(TestUnitReady));

	return(DoSCSICmd(DeviceRequest,Unit,&TestUnitReady,sizeof(TestUnitReady),NULL,0));
}

	/* CheckMediaType(struct IOStdReq *DeviceRequest,ULONG Unit):
	 *
	 *	Check if the device in question supports removable
	 *	media. However, this does not mean that the device is
	 *	capable of ejecting it on its own.
	 */

BOOL
CheckMediaType(struct IOStdReq *DeviceRequest,ULONG Unit)
{
	struct Inquiry		Inquiry;
	struct InquiryData	InquiryData;

	memset(&Inquiry,0,sizeof(Inquiry));

	Inquiry . Command			= 0x12;
	Inquiry . AllocationLength	= sizeof(struct InquiryData);

	if(!DoSCSICmd(DeviceRequest,Unit,&Inquiry,sizeof(struct Inquiry),&InquiryData,sizeof(struct InquiryData)))
	{
			/* A removable medium bit of zero indicates that the
			 * medium is not removable. A RMB bit of one indicates
			 * that the medium is removable.
			 */

		if(InquiryData . DeviceTypeModifier & 0x80)
			return(TRUE);
	}

	return(FALSE);
}

	/* EjectMedia(struct IOStdReq *DeviceRequest,ULONG Unit):
	 *
	 *	Eject the removable medium; this is accomplished via
	 *	the start/stop command. We request that the medium
	 *	should be unloaded after the unit is stopped. This
	 *	command is sometimes referred to as "load unload".
	 */

BYTE
EjectMedia(struct IOStdReq *DeviceRequest,ULONG Unit)
{
	struct StartStopUnit StartStopUnit;

	memset(&StartStopUnit,0,sizeof(StartStopUnit));

	StartStopUnit . OperationCode	= 0x1B;
	StartStopUnit . LoEj_Start		= 0x02;

	return(DoSCSICmd(DeviceRequest,Unit,&StartStopUnit,sizeof(StartStopUnit),NULL,0));
}

	/* Eject(const STRPTR DeviceName,ULONG Unit,ULONG Flags,BOOL Echo):
	 *
	 *	This routine does the dirty work. Open the device driver,
	 *	check for the device type, see if any media is loaded,
	 *	eject it if necessary.
	 */

BOOL
Eject(const STRPTR DeviceName,ULONG Unit,ULONG Flags,BOOL Echo)
{
	struct MsgPort	*DevicePort;
	BOOL			 Result = FALSE;

		/* Set up the interface data... */

	if(DevicePort = CreateMsgPort())
	{
		struct IOStdReq *DeviceRequest;

		if(DeviceRequest = (struct IOStdReq *)CreateIORequest(DevicePort,sizeof(struct IOStdReq)))
		{
			if(!OpenDevice(DeviceName,Unit,(struct IORequest *)DeviceRequest,Flags))
			{
				Result = TRUE;

					/* So far, so good. Now take a look if the device
					 * supports removable media.
					 */

				if(CheckMediaType(DeviceRequest,Unit))
				{
						/* Now check if there is any media loaded.
						 * We do the check twice in case the medium
						 * has just been loaded. To indicate this
						 * case, the SCSI standard requests that the
						 * device rejects the first command following
						 * the medium change.
						 */

					if(!TestUnitReady(DeviceRequest,Unit) && !TestUnitReady(DeviceRequest,Unit))
					{
							/* Go! */

						if(EjectMedia(DeviceRequest,Unit))
						{
							if(Echo)
								Printf("Eject: Error #%ld ejecting media.\n",DeviceRequest -> io_Error);

							Result = FALSE;
						}
					}
				}

					/* Clean up... */

				CloseDevice((struct IORequest *)DeviceRequest);
			}
			else
			{
				if(Echo)
					Printf("Eject: Error #%ld opening device \"%s\" unit #%ld.\n",DeviceRequest -> io_Error,DeviceName,Unit);
			}

			DeleteIORequest(DeviceRequest);
		}
		else
		{
			if(Echo)
				Printf("Eject: Cannot create IOStdReq.\n");
		}

		DeleteMsgPort(DevicePort);
	}
	else
	{
		if(Echo)
			Printf("Eject: Cannot create MsgPort.\n");
	}

	return(Result);
}

	/* FindDevice(...):
	 *
	 *	Find a device by name/filelock.
	 */

BOOL
FindDevice(const BPTR FileLock,const STRPTR FileName,STRPTR Device,ULONG *Unit,ULONG *Flags)
{
	struct DosList *Entry;

		/* For a start, lock the doslist entries we are interested in. */

	if(Entry = LockDosList(LDF_DEVICES | LDF_READ))
	{
			/* Do we have a filelock? */

		if(FileLock)
		{
			struct FileLock *LockPtr = (struct FileLock *)BADDR(FileLock);

				/* Walk down the list. */

			while(Entry = NextDosEntry(Entry,LDF_DEVICES | LDF_READ))
			{
					/* If the filing system task that manages this
					 * device equals the one the filelock originated
					 * from chances are very high that this is the
					 * one we were looking for. Please note that this
					 * technique is not absolutely bullet-proof as in
					 * theory it is possible to write a filing system
					 * which manages multiple devices. The task address
					 * might thus not necessarily point to the right
					 * doslist entry.
					 */

				if(Entry -> dol_Task == LockPtr -> fl_Task)
				{
					struct FileSysStartupMsg *Startup = (struct FileSysStartupMsg *)BADDR(Entry -> dol_misc . dol_handler . dol_Startup);

						/* Is this a valid entry? There might not be
						 * a pointer to the FSSM stored here; the
						 * Port-Handler for example uses this field
						 * to tell references to SER:, PAR: and PRT:
						 * apart.
						 */

					if(TypeOfMem(Startup))
					{
							/* Is this a valid device name? */

						if(TypeOfMem(BADDR(Startup -> fssm_Device)))
						{
							STRPTR Name = BADDR(Startup -> fssm_Device);

								/* Fill in the necessary data. */

							strcpy(Device,Name + 1);

							*Unit	= Startup -> fssm_Unit;
							*Flags	= Startup -> fssm_Flags;

								/* Release the list. */

							UnLockDosList(LDF_DEVICES | LDF_READ);

								/* Success. */

							return(TRUE);
						}
					}
				}
			}
		}
		else
		{
			UBYTE	LocalName[256];
			LONG	Len;

				/* Make a copy of the file name. */

			strcpy(LocalName,FileName);

			Len = strlen(LocalName);

				/* Remove the colon; we expect plain device
				 * names such as DF0, CD0 or HD0 here.
				 */

			if(LocalName[Len - 1] == ':')
				LocalName[Len - 1] = 0;

				/* Try to find the device entry in question. */

			if(Entry = FindDosEntry(Entry,LocalName,LDF_DEVICES | LDF_READ))
			{
				struct FileSysStartupMsg *Startup = (struct FileSysStartupMsg *)BADDR(Entry -> dol_misc . dol_handler . dol_Startup);

				if(TypeOfMem(Startup))
				{
					if(TypeOfMem(BADDR(Startup -> fssm_Device)))
					{
						STRPTR Name = BADDR(Startup -> fssm_Device);

						strcpy(Device,Name + 1);

						*Unit	= Startup -> fssm_Unit;
						*Flags	= Startup -> fssm_Flags;

						UnLockDosList(LDF_DEVICES | LDF_READ);

						return(TRUE);
					}
				}
			}
		}

		UnLockDosList(LDF_DEVICES | LDF_READ);
	}

	return(FALSE);
}
